Project: Identify Customer Segments

In this project, you will apply unsupervised learning techniques to identify segments of the population that form the core customer base for a mail-order sales company in Germany. These segments can then be used to direct marketing campaigns towards audiences that will have the highest expected rate of returns. The data that you will use has been provided by our partners at Bertelsmann Arvato Analytics, and represents a real-life data science task.

This notebook will help you complete this task by providing a framework within which you will perform your analysis steps. In each step of the project, you will see some text describing the subtask that you will perform, followed by one or more code cells for you to complete your work. Feel free to add additional code and markdown cells as you go along so that you can explore everything in precise chunks. The code cells provided in the base template will outline only the major tasks, and will usually not be enough to cover all of the minor tasks that comprise it.

It should be noted that while there will be precise guidelines on how you should handle certain tasks in the project, there will also be places where an exact specification is not provided. There will be times in the project where you will need to make and justify your own decisions on how to treat the data. These are places where there may not be only one way to handle the data. In real-life tasks, there may be many valid ways to approach an analysis task. One of the most important things you can do is clearly document your approach so that other scientists can understand the decisions you've made.

At the end of most sections, there will be a Markdown cell labeled Discussion. In these cells, you will report your findings for the completed section, as well as document the decisions that you made in your approach to each subtask. Your project will be evaluated not just on the code used to complete the tasks outlined, but also your communication about your observations and conclusions at each stage.

In [1]:
# import libraries here; add more as necessary
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
from pprint import pprint
from sklearn.preprocessing import StandardScaler, Imputer
from sklearn.decomposition import PCA
from sklearn.cluster import KMeans
from scipy.spatial.distance import cdist
from scipy.stats import itemfreq
from mpl_toolkits.mplot3d import Axes3D
# magic word for producing visualizations in notebook
%matplotlib inline

Step 0: Load the Data

There are four files associated with this project (not including this one):

  • Udacity_AZDIAS_Subset.csv: Demographics data for the general population of Germany; 891211 persons (rows) x 85 features (columns).
  • Udacity_CUSTOMERS_Subset.csv: Demographics data for customers of a mail-order company; 191652 persons (rows) x 85 features (columns).
  • Data_Dictionary.md: Detailed information file about the features in the provided datasets.
  • AZDIAS_Feature_Summary.csv: Summary of feature attributes for demographics data; 85 features (rows) x 4 columns

Each row of the demographics files represents a single person, but also includes information outside of individuals, including information about their household, building, and neighborhood. You will use this information to cluster the general population into groups with similar demographic properties. Then, you will see how the people in the customers dataset fit into those created clusters. The hope here is that certain clusters are over-represented in the customers data, as compared to the general population; those over-represented clusters will be assumed to be part of the core userbase. This information can then be used for further applications, such as targeting for a marketing campaign.

To start off with, load in the demographics data for the general population into a pandas DataFrame, and do the same for the feature attributes summary. Note for all of the .csv data files in this project: they're semicolon (;) delimited, so you'll need an additional argument in your read_csv() call to read in the data properly. Also, considering the size of the main dataset, it may take some time for it to load completely.

Once the dataset is loaded, it's recommended that you take a little bit of time just browsing the general structure of the dataset and feature summary file. You'll be getting deep into the innards of the cleaning in the first major step of the project, so gaining some general familiarity can help you get your bearings.

In [2]:
# Load in the general demographics data.
azdias = pd.read_csv('Udacity_AZDIAS_Subset.csv', delimiter=';')
azdias_df = azdias.copy()

# Load in the feature summary file.
feat_info = pd.read_csv('AZDIAS_Feature_Summary.csv', delimiter=';')
features_info_df = feat_info.copy()  
features_info_df.set_index('attribute', inplace=True)
features_info_df['columns'] = features_info_df.index
In [3]:
# Check the structure of the data after it's loaded (e.g. print the number of
# rows and columns, print the first few rows).
print(azdias_df.shape)
print(list(azdias_df.columns.values))
azdias_df.describe()
(891221, 85)
['AGER_TYP', 'ALTERSKATEGORIE_GROB', 'ANREDE_KZ', 'CJT_GESAMTTYP', 'FINANZ_MINIMALIST', 'FINANZ_SPARER', 'FINANZ_VORSORGER', 'FINANZ_ANLEGER', 'FINANZ_UNAUFFAELLIGER', 'FINANZ_HAUSBAUER', 'FINANZTYP', 'GEBURTSJAHR', 'GFK_URLAUBERTYP', 'GREEN_AVANTGARDE', 'HEALTH_TYP', 'LP_LEBENSPHASE_FEIN', 'LP_LEBENSPHASE_GROB', 'LP_FAMILIE_FEIN', 'LP_FAMILIE_GROB', 'LP_STATUS_FEIN', 'LP_STATUS_GROB', 'NATIONALITAET_KZ', 'PRAEGENDE_JUGENDJAHRE', 'RETOURTYP_BK_S', 'SEMIO_SOZ', 'SEMIO_FAM', 'SEMIO_REL', 'SEMIO_MAT', 'SEMIO_VERT', 'SEMIO_LUST', 'SEMIO_ERL', 'SEMIO_KULT', 'SEMIO_RAT', 'SEMIO_KRIT', 'SEMIO_DOM', 'SEMIO_KAEM', 'SEMIO_PFLICHT', 'SEMIO_TRADV', 'SHOPPER_TYP', 'SOHO_KZ', 'TITEL_KZ', 'VERS_TYP', 'ZABEOTYP', 'ALTER_HH', 'ANZ_PERSONEN', 'ANZ_TITEL', 'HH_EINKOMMEN_SCORE', 'KK_KUNDENTYP', 'W_KEIT_KIND_HH', 'WOHNDAUER_2008', 'ANZ_HAUSHALTE_AKTIV', 'ANZ_HH_TITEL', 'GEBAEUDETYP', 'KONSUMNAEHE', 'MIN_GEBAEUDEJAHR', 'OST_WEST_KZ', 'WOHNLAGE', 'CAMEO_DEUG_2015', 'CAMEO_DEU_2015', 'CAMEO_INTL_2015', 'KBA05_ANTG1', 'KBA05_ANTG2', 'KBA05_ANTG3', 'KBA05_ANTG4', 'KBA05_BAUMAX', 'KBA05_GBZ', 'BALLRAUM', 'EWDICHTE', 'INNENSTADT', 'GEBAEUDETYP_RASTER', 'KKK', 'MOBI_REGIO', 'ONLINE_AFFINITAET', 'REGIOTYP', 'KBA13_ANZAHL_PKW', 'PLZ8_ANTG1', 'PLZ8_ANTG2', 'PLZ8_ANTG3', 'PLZ8_ANTG4', 'PLZ8_BAUMAX', 'PLZ8_HHZ', 'PLZ8_GBZ', 'ARBEIT', 'ORTSGR_KLS9', 'RELAT_AB']
Out[3]:
AGER_TYP ALTERSKATEGORIE_GROB ANREDE_KZ CJT_GESAMTTYP FINANZ_MINIMALIST FINANZ_SPARER FINANZ_VORSORGER FINANZ_ANLEGER FINANZ_UNAUFFAELLIGER FINANZ_HAUSBAUER ... PLZ8_ANTG1 PLZ8_ANTG2 PLZ8_ANTG3 PLZ8_ANTG4 PLZ8_BAUMAX PLZ8_HHZ PLZ8_GBZ ARBEIT ORTSGR_KLS9 RELAT_AB
count 891221.000000 891221.000000 891221.000000 886367.000000 891221.000000 891221.000000 891221.000000 891221.000000 891221.000000 891221.000000 ... 774706.000000 774706.000000 774706.000000 774706.000000 774706.000000 774706.000000 774706.000000 794005.000000 794005.000000 794005.00000
mean -0.358435 2.777398 1.522098 3.632838 3.074528 2.821039 3.401106 3.033328 2.874167 3.075121 ... 2.253330 2.801858 1.595426 0.699166 1.943913 3.612821 3.381087 3.167854 5.293002 3.07222
std 1.198724 1.068775 0.499512 1.595021 1.321055 1.464749 1.322134 1.529603 1.486731 1.353248 ... 0.972008 0.920309 0.986736 0.727137 1.459654 0.973967 1.111598 1.002376 2.303739 1.36298
min -1.000000 1.000000 1.000000 1.000000 1.000000 1.000000 1.000000 1.000000 1.000000 1.000000 ... 0.000000 0.000000 0.000000 0.000000 1.000000 1.000000 1.000000 1.000000 0.000000 1.00000
25% -1.000000 2.000000 1.000000 2.000000 2.000000 1.000000 3.000000 2.000000 2.000000 2.000000 ... 1.000000 2.000000 1.000000 0.000000 1.000000 3.000000 3.000000 3.000000 4.000000 2.00000
50% -1.000000 3.000000 2.000000 4.000000 3.000000 3.000000 3.000000 3.000000 3.000000 3.000000 ... 2.000000 3.000000 2.000000 1.000000 1.000000 4.000000 3.000000 3.000000 5.000000 3.00000
75% -1.000000 4.000000 2.000000 5.000000 4.000000 4.000000 5.000000 5.000000 4.000000 4.000000 ... 3.000000 3.000000 2.000000 1.000000 3.000000 4.000000 4.000000 4.000000 7.000000 4.00000
max 3.000000 9.000000 2.000000 6.000000 5.000000 5.000000 5.000000 5.000000 5.000000 5.000000 ... 4.000000 4.000000 3.000000 2.000000 5.000000 5.000000 5.000000 9.000000 9.000000 9.00000

8 rows × 81 columns

Tip: Add additional cells to keep everything in reasonably-sized chunks! Keyboard shortcut esc --> a (press escape to enter command mode, then press the 'A' key) adds a new cell before the active cell, and esc --> b adds a new cell after the active cell. If you need to convert an active cell to a markdown cell, use esc --> m and to convert to a code cell, use esc --> y.

Step 1: Preprocessing

Step 1.1: Assess Missing Data

The feature summary file contains a summary of properties for each demographics data column. You will use this file to help you make cleaning decisions during this stage of the project. First of all, you should assess the demographics data in terms of missing data. Pay attention to the following points as you perform your analysis, and take notes on what you observe. Make sure that you fill in the Discussion cell with your findings and decisions at the end of each step that has one!

Step 1.1.1: Convert Missing Value Codes to NaNs

The fourth column of the feature attributes summary (loaded in above as feat_info) documents the codes from the data dictionary that indicate missing or unknown data. While the file encodes this as a list (e.g. [-1,0]), this will get read in as a string object. You'll need to do a little bit of parsing to make use of it to identify and clean the data. Convert data that matches a 'missing' or 'unknown' value code into a numpy NaN value. You might want to see how much data takes on a 'missing' or 'unknown' code, and how much data is naturally missing, as a point of interest.

As one more reminder, you are encouraged to add additional cells to break up your analysis into manageable chunks.

In [4]:
# Identify missing or unknown data values and convert them to NaNs.

def convert(string_list):
   
    missing_values_dict = {}
    conv = lambda x: x[1:-1].split(',')
    res = conv(string_list)
    
    for item in res:

        if item.replace('-','').isnumeric():
            missing_values_dict[int(item)] = np.nan
            
        if item.isalpha():
            missing_values_dict[str(item)] = np.nan
            
    
    return missing_values_dict



print(features_info_df.head(3))


missing_values = features_info_df[['columns','missing_or_unknown']].copy()
missing_values = missing_values.query('missing_or_unknown !="[]"')
missing_values['missing_or_unknown'] = missing_values['missing_or_unknown'].apply(convert)

missing_dict = {x[0]: x[1] for x in missing_values.itertuples(index=False)}
#missing_dict
missing_dict

for key,value in missing_dict.items():
    azdias_df[key].replace(value, inplace=True)

azdias_df.head(3)
                     information_level         type missing_or_unknown  \
attribute                                                                
AGER_TYP                        person  categorical             [-1,0]   
ALTERSKATEGORIE_GROB            person      ordinal           [-1,0,9]   
ANREDE_KZ                       person  categorical             [-1,0]   

                                   columns  
attribute                                   
AGER_TYP                          AGER_TYP  
ALTERSKATEGORIE_GROB  ALTERSKATEGORIE_GROB  
ANREDE_KZ                        ANREDE_KZ  
Out[4]:
AGER_TYP ALTERSKATEGORIE_GROB ANREDE_KZ CJT_GESAMTTYP FINANZ_MINIMALIST FINANZ_SPARER FINANZ_VORSORGER FINANZ_ANLEGER FINANZ_UNAUFFAELLIGER FINANZ_HAUSBAUER ... PLZ8_ANTG1 PLZ8_ANTG2 PLZ8_ANTG3 PLZ8_ANTG4 PLZ8_BAUMAX PLZ8_HHZ PLZ8_GBZ ARBEIT ORTSGR_KLS9 RELAT_AB
0 NaN 2.0 1 2.0 3 4 3 5 5 3 ... NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN
1 NaN 1.0 2 5.0 1 5 2 5 4 5 ... 2.0 3.0 2.0 1.0 1.0 5.0 4.0 3.0 5.0 4.0
2 NaN 3.0 2 3.0 1 4 1 2 3 5 ... 3.0 3.0 1.0 0.0 1.0 4.0 4.0 3.0 5.0 2.0

3 rows × 85 columns

Step 1.1.2: Assess Missing Data in Each Column

How much missing data is present in each column? There are a few columns that are outliers in terms of the proportion of values that are missing. You will want to use matplotlib's hist() function to visualize the distribution of missing value counts to find these columns. Identify and document these columns. While some of these columns might have justifications for keeping or re-encoding the data, for this project you should just remove them from the dataframe. (Feel free to make remarks about these outlier columns in the discussion, however!)

For the remaining features, are there any patterns in which columns have, or share, missing data?

In [5]:
# Perform an assessment of how much missing data there is in each column of the
# dataset.
azdias_df.isnull().sum(axis=1).hist(bins=30)
na_counts_per_column = azdias_df.isnull().sum(axis=0).to_frame()
na_counts_per_column = na_counts_per_column.T.apply(lambda x: x / azdias_df.shape[0])
In [6]:
# Investigate patterns in the amount of missing data in each column.
na_counts_per_column.T.plot(kind='bar', figsize=(20,10))
Out[6]:
<matplotlib.axes._subplots.AxesSubplot at 0x7f90cab8af98>
In [7]:
# remove the columns that holds 20% and more of missing data

missing_20 = [col for col in azdias_df.columns if (azdias_df[col].isnull().sum()/azdias.shape[0]) * 100 > 20]
print(missing_20)
['AGER_TYP', 'GEBURTSJAHR', 'TITEL_KZ', 'ALTER_HH', 'KK_KUNDENTYP', 'KBA05_BAUMAX']
In [8]:
# Remove the outlier columns from the dataset. (You'll perform other data
# engineering tasks such as re-encoding and imputation later.)

for col in missing_20:
    azdias_df.drop(col, axis=1, inplace=True)

na_counts_per_column_new = azdias_df.isnull().sum(axis=0).to_frame()
na_counts_per_column_new = na_counts_per_column_new.T.apply(lambda x: x / azdias_df.shape[0])
    
ax = na_counts_per_column_new.T.plot(kind='bar', figsize=(30,10))
for p in ax.patches:
    ax.annotate("%.2f" % p.get_height(), (p.get_x() + p.get_width() / 2., p.get_height()), ha='center', va='center', xytext=(0, 10), textcoords='offset points')

Discussion 1.1.2: Assess Missing Data in Each Column

based on above graphs we could conclude the following

  1. some columns has high coorelation such as KBA05 related columns and PLZ8 related columns, as the ratios of missing data in their related columns are the same

  2. in total 6 columns have been removed as listed below

    • AGER_TYP
    • GEBURTSJAHR
    • TITEL_KZ
    • KK_KUNDENTYP
    • KBA05_BAUMAX
    • ALTER_HH

Step 1.1.3: Assess Missing Data in Each Row

Now, you'll perform a similar assessment for the rows of the dataset. How much data is missing in each row? As with the columns, you should see some groups of points that have a very different numbers of missing values. Divide the data into two subsets: one for data points that are above some threshold for missing values, and a second subset for points below that threshold.

In order to know what to do with the outlier rows, we should see if the distribution of data values on columns that are not missing data (or are missing very little data) are similar or different between the two groups. Select at least five of these columns and compare the distribution of values.

  • You can use seaborn's countplot() function to create a bar chart of code frequencies and matplotlib's subplot() function to put bar charts for the two subplots side by side.
  • To reduce repeated code, you might want to write a function that can perform this comparison, taking as one of its arguments a column to be compared.

Depending on what you observe in your comparison, this will have implications on how you approach your conclusions later in the analysis. If the distributions of non-missing features look similar between the data with many missing values and the data with few or no missing values, then we could argue that simply dropping those points from the analysis won't present a major issue. On the other hand, if the data with many missing values looks very different from the data with few or no missing values, then we should make a note on those data as special. We'll revisit these data later on. Either way, you should continue your analysis for now using just the subset of the data with few or no missing values.

In [9]:
# How much data is missing in each row of the dataset?
missing_in_row = azdias.isnull().sum(axis=1)
missing_in_row = missing_in_row[missing_in_row > 0]/(len(azdias_df.columns)) * 100

plt.hist(missing_in_row, bins=10, facecolor='g', alpha=0.75)


plt.xlabel('missing values(%)')
plt.ylabel('Counts')
plt.title('missing value counts in rows')
plt.grid(True)
plt.show()
In [10]:
# Write code to divide the data into two subsets based on the number of missing
# values in each row.


missing_data_le_2 = azdias_df[azdias_df.isnull().sum(axis=1) < 2].reset_index(drop=True)
missing_data_gt_2 = azdias_df[azdias_df.isnull().sum(axis=1) >= 2].reset_index(drop=True)

print(missing_data_le_2.shape)
missing_data_gt_2.head(3)
(638947, 79)
Out[10]:
ALTERSKATEGORIE_GROB ANREDE_KZ CJT_GESAMTTYP FINANZ_MINIMALIST FINANZ_SPARER FINANZ_VORSORGER FINANZ_ANLEGER FINANZ_UNAUFFAELLIGER FINANZ_HAUSBAUER FINANZTYP ... PLZ8_ANTG1 PLZ8_ANTG2 PLZ8_ANTG3 PLZ8_ANTG4 PLZ8_BAUMAX PLZ8_HHZ PLZ8_GBZ ARBEIT ORTSGR_KLS9 RELAT_AB
0 2.0 1 2.0 3 4 3 5 5 3 4 ... NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN
1 4.0 2 2.0 4 2 5 2 1 2 6 ... 2.0 2.0 2.0 0.0 1.0 3.0 4.0 2.0 3.0 3.0
2 2.0 1 6.0 3 4 3 5 5 3 4 ... NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN

3 rows × 79 columns

In [11]:
# Compare the distribution of values for at least five columns where there are
# no or few missing values, between the two subsets.

row = 15 
col = 2 
fig, ax =plt.subplots(nrows=row, ncols=col, figsize = (20,30))
index = 0

if index < 15: 
    for i in range(row): 
        sns.countplot(x=missing_data_gt_2.columns[index],data=missing_data_gt_2, ax=ax[i][0])
        sns.countplot(x=missing_data_le_2.columns[index],data=missing_data_le_2, ax=ax[i][1])

        index = index + 1
    
ax[0][0].set_title('alot of NaNs / row') 
ax[0][1].set_title('few NaNs / row') 
fig.tight_layout() 
fig.show()
/opt/conda/lib/python3.6/site-packages/matplotlib/figure.py:418: UserWarning: matplotlib is currently using a non-GUI backend, so cannot show the figure
  "matplotlib is currently using a non-GUI backend, "

Discussion 1.1.3: Assess Missing Data in Each Row

Looking at avarages of missing data per row, it seems that most rows have around 5%. I set the threshold to 2 missing values in each row, knowing that we are going to loose a lot of data but the remaining data is cleaner

  1. It seems that the populations are pretty similar in age and gender, Life stage, , but are pretty different when it comes to financial spending patterns
  2. the data is qualitatively different when it comes to finance, Vacation habits, and preferred information and buying channels for consumer

Step 1.2: Select and Re-Encode Features

Checking for missing data isn't the only way in which you can prepare a dataset for analysis. Since the unsupervised learning techniques to be used will only work on data that is encoded numerically, you need to make a few encoding changes or additional assumptions to be able to make progress. In addition, while almost all of the values in the dataset are encoded using numbers, not all of them represent numeric values. Check the third column of the feature summary (feat_info) for a summary of types of measurement.

  • For numeric and interval data, these features can be kept without changes.
  • Most of the variables in the dataset are ordinal in nature. While ordinal values may technically be non-linear in spacing, make the simplifying assumption that the ordinal variables can be treated as being interval in nature (that is, kept without any changes).
  • Special handling may be necessary for the remaining two variable types: categorical, and 'mixed'.

In the first two parts of this sub-step, you will perform an investigation of the categorical and mixed-type features and make a decision on each of them, whether you will keep, drop, or re-encode each. Then, in the last part, you will create a new data frame with only the selected and engineered columns.

Data wrangling is often the trickiest part of the data analysis process, and there's a lot of it to be done here. But stick with it: once you're done with this step, you'll be ready to get to the machine learning parts of the project!

In [12]:
# How many features are there of each data type?

types={}
for col in missing_data_le_2.columns:
    data_type = features_info_df.loc[col].type
    if data_type not in types:
        types[data_type] = 1
    else:
        types[data_type] += 1

print(types)
{'ordinal': 49, 'categorical': 18, 'mixed': 6, 'numeric': 6}

Step 1.2.1: Re-Encode Categorical Features

For categorical data, you would ordinarily need to encode the levels as dummy variables. Depending on the number of categories, perform one of the following:

  • For binary (two-level) categoricals that take numeric values, you can keep them without needing to do anything.
  • There is one binary variable that takes on non-numeric values. For this one, you need to re-encode the values as numbers or create a dummy variable.
  • For multi-level categoricals (three or more values), you can choose to encode the values using multiple dummy variables (e.g. via OneHotEncoder), or (to keep things straightforward) just drop them from the analysis. As always, document your choices in the Discussion section.
In [13]:
# Assess categorical variables: which are binary, which are multi-level, and
# which one needs to be re-encoded?

bin_list = [] 
multi_level_list = [] 

filtered_features = features_info_df.loc[missing_data_le_2.columns]

cat_variable = missing_data_le_2[filtered_features[filtered_features['type'] == 'categorical']['columns']] 
cat_variable

for t in cat_variable: 
    if len(cat_variable[t].value_counts()) == 2 and  (
        set(cat_variable[t]) & set([0, 1]) == set([0, 1]) or 
        set(cat_variable[t]) & set([0.0, 1.0]) == set([0.0, 1.0])
    ): 
        bin_list.append(t) 
    else: 
        multi_level_list.append(t)


print(cat_variable.info())
print('-'*50)
pprint('Binary: {}'.format(bin_list))
print('')
pprint('multi-level: {}'.format(multi_level_list))
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 638947 entries, 0 to 638946
Data columns (total 18 columns):
ANREDE_KZ           638947 non-null int64
CJT_GESAMTTYP       638947 non-null float64
FINANZTYP           638947 non-null int64
GFK_URLAUBERTYP     638947 non-null float64
GREEN_AVANTGARDE    638947 non-null int64
LP_FAMILIE_FEIN     638947 non-null float64
LP_FAMILIE_GROB     638947 non-null float64
LP_STATUS_FEIN      638947 non-null float64
LP_STATUS_GROB      638947 non-null float64
NATIONALITAET_KZ    638947 non-null float64
SHOPPER_TYP         638947 non-null float64
SOHO_KZ             638947 non-null float64
VERS_TYP            638947 non-null float64
ZABEOTYP            638947 non-null int64
GEBAEUDETYP         638947 non-null float64
OST_WEST_KZ         638947 non-null object
CAMEO_DEUG_2015     638947 non-null object
CAMEO_DEU_2015      638947 non-null object
dtypes: float64(11), int64(4), object(3)
memory usage: 87.7+ MB
None
--------------------------------------------------
"Binary: ['GREEN_AVANTGARDE', 'SOHO_KZ']"

("multi-level: ['ANREDE_KZ', 'CJT_GESAMTTYP', 'FINANZTYP', 'GFK_URLAUBERTYP', "
 "'LP_FAMILIE_FEIN', 'LP_FAMILIE_GROB', 'LP_STATUS_FEIN', 'LP_STATUS_GROB', "
 "'NATIONALITAET_KZ', 'SHOPPER_TYP', 'VERS_TYP', 'ZABEOTYP', 'GEBAEUDETYP', "
 "'OST_WEST_KZ', 'CAMEO_DEUG_2015', 'CAMEO_DEU_2015']")
In [14]:
missing_data_le_2[['GREEN_AVANTGARDE','SOHO_KZ','ANREDE_KZ']].T
Out[14]:
0 1 2 3 4 5 6 7 8 9 ... 638937 638938 638939 638940 638941 638942 638943 638944 638945 638946
GREEN_AVANTGARDE 0.0 1.0 0.0 0.0 0.0 0.0 1.0 1.0 0.0 1.0 ... 1.0 0.0 0.0 0.0 0.0 1.0 0.0 0.0 0.0 0.0
SOHO_KZ 1.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 ... 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0
ANREDE_KZ 2.0 2.0 1.0 2.0 2.0 1.0 1.0 2.0 2.0 1.0 ... 2.0 2.0 1.0 1.0 1.0 2.0 2.0 2.0 1.0 1.0

3 rows × 638947 columns

In [15]:
# Re-encode categorical variable(s) to be kept in the analysis.

# encode binary
df_with_dummies = pd.get_dummies( missing_data_le_2, columns = bin_list ,drop_first=True)
# drop multi-level
for col in multi_level_list:
    df_with_dummies.drop(col, axis=1, inplace=True)

print(df_with_dummies.columns.tolist())

df_with_dummies.shape
['ALTERSKATEGORIE_GROB', 'FINANZ_MINIMALIST', 'FINANZ_SPARER', 'FINANZ_VORSORGER', 'FINANZ_ANLEGER', 'FINANZ_UNAUFFAELLIGER', 'FINANZ_HAUSBAUER', 'HEALTH_TYP', 'LP_LEBENSPHASE_FEIN', 'LP_LEBENSPHASE_GROB', 'PRAEGENDE_JUGENDJAHRE', 'RETOURTYP_BK_S', 'SEMIO_SOZ', 'SEMIO_FAM', 'SEMIO_REL', 'SEMIO_MAT', 'SEMIO_VERT', 'SEMIO_LUST', 'SEMIO_ERL', 'SEMIO_KULT', 'SEMIO_RAT', 'SEMIO_KRIT', 'SEMIO_DOM', 'SEMIO_KAEM', 'SEMIO_PFLICHT', 'SEMIO_TRADV', 'ANZ_PERSONEN', 'ANZ_TITEL', 'HH_EINKOMMEN_SCORE', 'W_KEIT_KIND_HH', 'WOHNDAUER_2008', 'ANZ_HAUSHALTE_AKTIV', 'ANZ_HH_TITEL', 'KONSUMNAEHE', 'MIN_GEBAEUDEJAHR', 'WOHNLAGE', 'CAMEO_INTL_2015', 'KBA05_ANTG1', 'KBA05_ANTG2', 'KBA05_ANTG3', 'KBA05_ANTG4', 'KBA05_GBZ', 'BALLRAUM', 'EWDICHTE', 'INNENSTADT', 'GEBAEUDETYP_RASTER', 'KKK', 'MOBI_REGIO', 'ONLINE_AFFINITAET', 'REGIOTYP', 'KBA13_ANZAHL_PKW', 'PLZ8_ANTG1', 'PLZ8_ANTG2', 'PLZ8_ANTG3', 'PLZ8_ANTG4', 'PLZ8_BAUMAX', 'PLZ8_HHZ', 'PLZ8_GBZ', 'ARBEIT', 'ORTSGR_KLS9', 'RELAT_AB', 'GREEN_AVANTGARDE_1', 'SOHO_KZ_1.0']
Out[15]:
(638947, 63)

Discussion 1.2.1: Re-Encode Categorical Features

  1. garph the variability in the dataset as it could indicate categorical results, on the other hand it helps to visualize the categories
  2. split the categorical columns between binary(2 values "0/1 , 0.0, 1.0"), and multi-level (> 2 values) based on AZDIAS_Feature_Summary.csv file
  3. refered back to Data_Dictionary.md and the categories garph above to check the values
  4. decided with dropping all the multi-level features
  5. i used get_dummies for encoding with dropping the first accurance

Step 1.2.2: Engineer Mixed-Type Features

There are a handful of features that are marked as "mixed" in the feature summary that require special treatment in order to be included in the analysis. There are two in particular that deserve attention; the handling of the rest are up to your own choices:

  • "PRAEGENDE_JUGENDJAHRE" combines information on three dimensions: generation by decade, movement (mainstream vs. avantgarde), and nation (east vs. west). While there aren't enough levels to disentangle east from west, you should create two new variables to capture the other two dimensions: an interval-type variable for decade, and a binary variable for movement.
  • "CAMEO_INTL_2015" combines information on two axes: wealth and life stage. Break up the two-digit codes by their 'tens'-place and 'ones'-place digits into two new ordinal variables (which, for the purposes of this project, is equivalent to just treating them as their raw numeric values).
  • If you decide to keep or engineer new features around the other mixed-type features, make sure you note your steps in the Discussion section.

Be sure to check Data_Dictionary.md for the details needed to finish these tasks.

In [16]:
# Investigate "PRAEGENDE_JUGENDJAHRE" and engineer two new variables.
first_var_dict = {
    -1:{'period': np.nan, 'direction': np.nan},
    0:{'period': np.nan, 'direction': np.nan},
    1: {'period': 40, 'direction': 'EW'},
    2: {'period': 40, 'direction': 'EW'},
    3: {'period': 50, 'direction': 'EW'},
    4: {'period': 50, 'direction': 'EW'},
    5: {'period': 60, 'direction': 'EW'},
    6: {'period': 60, 'direction': 'W'},
    7: {'period': 60, 'direction': 'E'},
    8: {'period': 70, 'direction': 'EW'},
    9: {'period': 70, 'direction': 'EW'},
    10: {'period': 80, 'direction': 'W'},
    11: {'period': 80, 'direction': 'W'},
    12: {'period': 80, 'direction': 'E'},
    13: {'period': 80, 'direction': 'E'},
    14: {'period': 90, 'direction': 'EW'},
    15: {'period': 90, 'direction': 'EW'}
}

#initialize two columns
df_with_dummies['PRAEGENDE_JUGENDJAHRE_PERIOD']= 0
df_with_dummies['PRAEGENDE_JUGENDJAHRE_DIRECTION'] = 'x'


for key, values in first_var_dict.items():
    #df_with_dummies[['PRAEGENDE_JUGENDJAHRE_PERIOD','PRAEGENDE_JUGENDJAHRE_DIRECTION']] = 
    row_indexes = df_with_dummies[df_with_dummies['PRAEGENDE_JUGENDJAHRE'] == key].index
    df_with_dummies.loc[row_indexes,'PRAEGENDE_JUGENDJAHRE_PERIOD']= values['period']
    df_with_dummies.loc[row_indexes,'PRAEGENDE_JUGENDJAHRE_DIRECTION']= values['direction']

# check missing
print(f'missing values for [PRAEGENDE_JUGENDJAHRE_PERIOD]: ', df_with_dummies[df_with_dummies['PRAEGENDE_JUGENDJAHRE_PERIOD'] == 0].shape[0])
print('missing values for [PRAEGENDE_JUGENDJAHRE_DIRECTION]: ', df_with_dummies[df_with_dummies['PRAEGENDE_JUGENDJAHRE_DIRECTION'] == 'x'].shape[0])

# drop the original column
df_with_dummies.drop(['PRAEGENDE_JUGENDJAHRE'], axis=1, inplace=True)

#create dummy for direction
df_with_dummies = pd.get_dummies( df_with_dummies, columns = ['PRAEGENDE_JUGENDJAHRE_DIRECTION'] ,drop_first=True)
missing values for [PRAEGENDE_JUGENDJAHRE_PERIOD]:  6226
missing values for [PRAEGENDE_JUGENDJAHRE_DIRECTION]:  6226
In [17]:
df_with_dummies['CAMEO_INTL_2015'].value_counts(dropna=False)
Out[17]:
51    114736
41     76608
24     75385
14     49195
43     47588
54     39345
25     31483
22     24326
45     22420
55     20049
13     19922
52     17097
34     15529
23     14926
15     13752
31     12673
44     12419
35      8631
32      8287
33      7575
12      7001
Name: CAMEO_INTL_2015, dtype: int64
In [18]:
# Investigate "CAMEO_INTL_2015" and engineer two new variables.

#  convert the type of CAMEO_INTL_2015 from int to integer
#df_with_dummies['CAMEO_INTL_2015'] = df_with_dummies['CAMEO_INTL_2015'].astype(int)

#initialize columns

def get_life_stage(number):
    if number == '-1' or number == 'XX':
        return np.nan
    return int(number) % 10

def get_wealth(number):
    if number == '-1' or number == 'XX':
        return np.nan
    
    number = int(number) //10
    return number % 10

df_with_dummies['CAMEO_INTL_2015_WEALTH'] = df_with_dummies['CAMEO_INTL_2015'].apply(get_wealth)
df_with_dummies['CAMEO_INTL_2015_LIFE_STAGE'] = df_with_dummies['CAMEO_INTL_2015'].apply(get_life_stage)

print(df_with_dummies[['CAMEO_INTL_2015', 'CAMEO_INTL_2015_WEALTH', 'CAMEO_INTL_2015_LIFE_STAGE']].head(10).T)

df_with_dummies.drop('CAMEO_INTL_2015', axis=1, inplace=True)
                             0   1   2   3   4   5   6   7   8   9
CAMEO_INTL_2015             51  24  43  54  22  14  13  15  51  34
CAMEO_INTL_2015_WEALTH       5   2   4   5   2   1   1   1   5   3
CAMEO_INTL_2015_LIFE_STAGE   1   4   3   4   2   4   3   5   1   4
In [19]:
# drop the remaining mixed categories

mixed_features_df = features_info_df.loc[missing_data_le_2.columns][['columns','type']].query('type == "mixed"')

columns = set(mixed_features_df['columns'].tolist())
columns = columns - set(['CAMEO_INTL_2015', 'PRAEGENDE_JUGENDJAHRE'])

pprint(f'columns that will be dropped: {columns}')

df_with_dummies.drop(columns, axis=1, inplace=True)


# check if columns still there
columns in set(df_with_dummies.columns)
("columns that will be dropped: {'WOHNLAGE', 'PLZ8_BAUMAX', "
 "'LP_LEBENSPHASE_FEIN', 'LP_LEBENSPHASE_GROB'}")
Out[19]:
False
In [20]:
df_with_dummies.shape
Out[20]:
(638947, 63)

Discussion 1.2.2: Engineer Mixed-Type Features

(Double-click this cell and replace this text with your own text, reporting your findings and decisions regarding mixed-value features. Which ones did you keep, which did you drop, and what engineering steps did you perform?)

  1. i dropped all mixed features other than the two features that have been engineered in the privious steps
  2. engineering steps for PRAEGENDE_JUGENDJAHRE:
    • create a dictionary for the values to be inserted in the generated features
    • initialize the new featires in the dataframe
    • loop over the dictionary keys/values where the key in the dict = the value in the feature that we want to process
    • extract the indecies for the rows that mathes the values, and append proper values for the new 2 features base on the dict values
    • drop the original column
  3. engineering steps for CAMEO_INTL_2015
    • create to functions to extract features values from the original feature
    • apply those two functions to the original feature to fill the new features
  4. dropped features
    • extract the mixed features from feat_info
    • get the columns and convert them to set
    • subtract the two columns that we already processed in the previous steps
    • drop the remaining columns

Step 1.2.3: Complete Feature Selection

In order to finish this step up, you need to make sure that your data frame now only has the columns that you want to keep. To summarize, the dataframe should consist of the following:

  • All numeric, interval, and ordinal type columns from the original dataset.
  • Binary categorical features (all numerically-encoded).
  • Engineered features from other multi-level categorical features and mixed features.

Make sure that for any new columns that you have engineered, that you've excluded the original columns from the final dataset. Otherwise, their values will interfere with the analysis later on the project. For example, you should not keep "PRAEGENDE_JUGENDJAHRE", since its values won't be useful for the algorithm: only the values derived from it in the engineered features you created should be retained. As a reminder, your data should only be from the subset with few or no missing values.

In [21]:
# If there are other re-engineering tasks you need to perform, make sure you
# take care of them here. (Dealing with missing data will come in step 2.1.)

features = feat_info[['attribute','type']]
columns = features['attribute'].tolist()

dict_of_remaining_cols = {}

for col in columns:
    if col in df_with_dummies or df_with_dummies.columns.str.startswith(col).any():
        df_columns = df_with_dummies.filter(regex=f'{col}.*').columns.tolist()
        dict_of_remaining_cols[col] = { 
            'category' :features[features['attribute'] == col ].iloc[0][1],
            'columns': df_columns
        }

pprint(f'size of the featires {df_with_dummies.shape[1]}')
print('-'*50)
pprint(dict_of_remaining_cols)

pprint(dict_of_remaining_cols.keys())
'size of the featires 63'
--------------------------------------------------
{'ALTERSKATEGORIE_GROB': {'category': 'ordinal',
                          'columns': ['ALTERSKATEGORIE_GROB']},
 'ANZ_HAUSHALTE_AKTIV': {'category': 'numeric',
                         'columns': ['ANZ_HAUSHALTE_AKTIV']},
 'ANZ_HH_TITEL': {'category': 'numeric', 'columns': ['ANZ_HH_TITEL']},
 'ANZ_PERSONEN': {'category': 'numeric', 'columns': ['ANZ_PERSONEN']},
 'ANZ_TITEL': {'category': 'numeric', 'columns': ['ANZ_TITEL']},
 'ARBEIT': {'category': 'ordinal', 'columns': ['ARBEIT']},
 'BALLRAUM': {'category': 'ordinal', 'columns': ['BALLRAUM']},
 'CAMEO_INTL_2015': {'category': 'mixed',
                     'columns': ['CAMEO_INTL_2015_WEALTH',
                                 'CAMEO_INTL_2015_LIFE_STAGE']},
 'EWDICHTE': {'category': 'ordinal', 'columns': ['EWDICHTE']},
 'FINANZ_ANLEGER': {'category': 'ordinal', 'columns': ['FINANZ_ANLEGER']},
 'FINANZ_HAUSBAUER': {'category': 'ordinal', 'columns': ['FINANZ_HAUSBAUER']},
 'FINANZ_MINIMALIST': {'category': 'ordinal', 'columns': ['FINANZ_MINIMALIST']},
 'FINANZ_SPARER': {'category': 'ordinal', 'columns': ['FINANZ_SPARER']},
 'FINANZ_UNAUFFAELLIGER': {'category': 'ordinal',
                           'columns': ['FINANZ_UNAUFFAELLIGER']},
 'FINANZ_VORSORGER': {'category': 'ordinal', 'columns': ['FINANZ_VORSORGER']},
 'GEBAEUDETYP': {'category': 'categorical', 'columns': ['GEBAEUDETYP_RASTER']},
 'GEBAEUDETYP_RASTER': {'category': 'ordinal',
                        'columns': ['GEBAEUDETYP_RASTER']},
 'GREEN_AVANTGARDE': {'category': 'categorical',
                      'columns': ['GREEN_AVANTGARDE_1']},
 'HEALTH_TYP': {'category': 'ordinal', 'columns': ['HEALTH_TYP']},
 'HH_EINKOMMEN_SCORE': {'category': 'ordinal',
                        'columns': ['HH_EINKOMMEN_SCORE']},
 'INNENSTADT': {'category': 'ordinal', 'columns': ['INNENSTADT']},
 'KBA05_ANTG1': {'category': 'ordinal', 'columns': ['KBA05_ANTG1']},
 'KBA05_ANTG2': {'category': 'ordinal', 'columns': ['KBA05_ANTG2']},
 'KBA05_ANTG3': {'category': 'ordinal', 'columns': ['KBA05_ANTG3']},
 'KBA05_ANTG4': {'category': 'ordinal', 'columns': ['KBA05_ANTG4']},
 'KBA05_GBZ': {'category': 'ordinal', 'columns': ['KBA05_GBZ']},
 'KBA13_ANZAHL_PKW': {'category': 'numeric', 'columns': ['KBA13_ANZAHL_PKW']},
 'KKK': {'category': 'ordinal', 'columns': ['KKK']},
 'KONSUMNAEHE': {'category': 'ordinal', 'columns': ['KONSUMNAEHE']},
 'MIN_GEBAEUDEJAHR': {'category': 'numeric', 'columns': ['MIN_GEBAEUDEJAHR']},
 'MOBI_REGIO': {'category': 'ordinal', 'columns': ['MOBI_REGIO']},
 'ONLINE_AFFINITAET': {'category': 'ordinal', 'columns': ['ONLINE_AFFINITAET']},
 'ORTSGR_KLS9': {'category': 'ordinal', 'columns': ['ORTSGR_KLS9']},
 'PLZ8_ANTG1': {'category': 'ordinal', 'columns': ['PLZ8_ANTG1']},
 'PLZ8_ANTG2': {'category': 'ordinal', 'columns': ['PLZ8_ANTG2']},
 'PLZ8_ANTG3': {'category': 'ordinal', 'columns': ['PLZ8_ANTG3']},
 'PLZ8_ANTG4': {'category': 'ordinal', 'columns': ['PLZ8_ANTG4']},
 'PLZ8_GBZ': {'category': 'ordinal', 'columns': ['PLZ8_GBZ']},
 'PLZ8_HHZ': {'category': 'ordinal', 'columns': ['PLZ8_HHZ']},
 'PRAEGENDE_JUGENDJAHRE': {'category': 'mixed',
                           'columns': ['PRAEGENDE_JUGENDJAHRE_PERIOD',
                                       'PRAEGENDE_JUGENDJAHRE_DIRECTION_EW',
                                       'PRAEGENDE_JUGENDJAHRE_DIRECTION_W',
                                       'PRAEGENDE_JUGENDJAHRE_DIRECTION_x']},
 'REGIOTYP': {'category': 'ordinal', 'columns': ['REGIOTYP']},
 'RELAT_AB': {'category': 'ordinal', 'columns': ['RELAT_AB']},
 'RETOURTYP_BK_S': {'category': 'ordinal', 'columns': ['RETOURTYP_BK_S']},
 'SEMIO_DOM': {'category': 'ordinal', 'columns': ['SEMIO_DOM']},
 'SEMIO_ERL': {'category': 'ordinal', 'columns': ['SEMIO_ERL']},
 'SEMIO_FAM': {'category': 'ordinal', 'columns': ['SEMIO_FAM']},
 'SEMIO_KAEM': {'category': 'ordinal', 'columns': ['SEMIO_KAEM']},
 'SEMIO_KRIT': {'category': 'ordinal', 'columns': ['SEMIO_KRIT']},
 'SEMIO_KULT': {'category': 'ordinal', 'columns': ['SEMIO_KULT']},
 'SEMIO_LUST': {'category': 'ordinal', 'columns': ['SEMIO_LUST']},
 'SEMIO_MAT': {'category': 'ordinal', 'columns': ['SEMIO_MAT']},
 'SEMIO_PFLICHT': {'category': 'ordinal', 'columns': ['SEMIO_PFLICHT']},
 'SEMIO_RAT': {'category': 'ordinal', 'columns': ['SEMIO_RAT']},
 'SEMIO_REL': {'category': 'ordinal', 'columns': ['SEMIO_REL']},
 'SEMIO_SOZ': {'category': 'ordinal', 'columns': ['SEMIO_SOZ']},
 'SEMIO_TRADV': {'category': 'ordinal', 'columns': ['SEMIO_TRADV']},
 'SEMIO_VERT': {'category': 'ordinal', 'columns': ['SEMIO_VERT']},
 'SOHO_KZ': {'category': 'categorical', 'columns': ['SOHO_KZ_1.0']},
 'WOHNDAUER_2008': {'category': 'ordinal', 'columns': ['WOHNDAUER_2008']},
 'W_KEIT_KIND_HH': {'category': 'ordinal', 'columns': ['W_KEIT_KIND_HH']}}
dict_keys(['ALTERSKATEGORIE_GROB', 'FINANZ_MINIMALIST', 'FINANZ_SPARER', 'FINANZ_VORSORGER', 'FINANZ_ANLEGER', 'FINANZ_UNAUFFAELLIGER', 'FINANZ_HAUSBAUER', 'GREEN_AVANTGARDE', 'HEALTH_TYP', 'PRAEGENDE_JUGENDJAHRE', 'RETOURTYP_BK_S', 'SEMIO_SOZ', 'SEMIO_FAM', 'SEMIO_REL', 'SEMIO_MAT', 'SEMIO_VERT', 'SEMIO_LUST', 'SEMIO_ERL', 'SEMIO_KULT', 'SEMIO_RAT', 'SEMIO_KRIT', 'SEMIO_DOM', 'SEMIO_KAEM', 'SEMIO_PFLICHT', 'SEMIO_TRADV', 'SOHO_KZ', 'ANZ_PERSONEN', 'ANZ_TITEL', 'HH_EINKOMMEN_SCORE', 'W_KEIT_KIND_HH', 'WOHNDAUER_2008', 'ANZ_HAUSHALTE_AKTIV', 'ANZ_HH_TITEL', 'GEBAEUDETYP', 'KONSUMNAEHE', 'MIN_GEBAEUDEJAHR', 'CAMEO_INTL_2015', 'KBA05_ANTG1', 'KBA05_ANTG2', 'KBA05_ANTG3', 'KBA05_ANTG4', 'KBA05_GBZ', 'BALLRAUM', 'EWDICHTE', 'INNENSTADT', 'GEBAEUDETYP_RASTER', 'KKK', 'MOBI_REGIO', 'ONLINE_AFFINITAET', 'REGIOTYP', 'KBA13_ANZAHL_PKW', 'PLZ8_ANTG1', 'PLZ8_ANTG2', 'PLZ8_ANTG3', 'PLZ8_ANTG4', 'PLZ8_HHZ', 'PLZ8_GBZ', 'ARBEIT', 'ORTSGR_KLS9', 'RELAT_AB'])
In [22]:
# Do whatever you need to in order to ensure that the dataframe only contains
# the columns that should be passed to the algorithm functions.

# remove the columns that has been agreed on in section 1.1.2 if they still exist 
# GEBURTSJAHR, TITEL_KZ, KK_KUNDENTYP, KBA05_BAUMAX, ALTER_HH but before check if they are ordinal or interval
cols = ['AGER_TYP','GEBURTSJAHR','TITEL_KZ','KK_KUNDENTYP','KBA05_BAUMAX','ALTER_HH']

for c in cols:
    if c in dict_of_remaining_cols.keys() and dict_of_remaining_cols[c]['category']== 'categorical':
        df_with_dummies.drop(dict_of_remaining_cols[c]['columns'], axis=1, inplace=True)
        print(f'dropping: {dict_of_remaining_cols[c]["columns"]}')
        
    
df_with_dummies.shape
Out[22]:
(638947, 63)

Step 1.3: Create a Cleaning Function

Even though you've finished cleaning up the general population demographics data, it's important to look ahead to the future and realize that you'll need to perform the same cleaning steps on the customer demographics data. In this substep, complete the function below to execute the main feature selection, encoding, and re-engineering steps you performed above. Then, when it comes to looking at the customer data in Step 3, you can just run this function on that DataFrame to get the trimmed dataset in a single step.

In [23]:
# convert string list to related list of numerics or strings based
import math
def convert(string_list):
   
    missing_values_dict = {}
    conv = lambda x: x[1:-1].split(',')
    res = conv(string_list)
    
    for item in res:

        if item.replace('-','').isnumeric():
            missing_values_dict[int(item)] = np.nan
            
        if item.isalpha():
            missing_values_dict[str(item)] = np.nan
            
    return missing_values_dict

# related to extract life_stage feature from CAMEO_INTL_2015
def get_life_stage(number):
    if number == '-1' or number == 'XX' or math.isnan(float(number)):
        return np.nan
    return int(number) % 10

# related to extract wealth feature from CAMEO_INTL_2015
def get_wealth(number):
    if number == '-1' or number == 'XX' or math.isnan(float(number)):
        return np.nan
    
    number = int(number) //10
    return number % 10

# extract missing features values from feature_info and construct a dictionary from the values
def get_missing_features_dict_from_features_info(features_info_df):
    missing_values = features_info_df[['attribute','missing_or_unknown']].copy()
    missing_values = missing_values.query('missing_or_unknown !="[]"')
    missing_values['missing_or_unknown'] = missing_values['missing_or_unknown'].apply(convert)
    
    missing_dict = {x[0]: x[1] for x in missing_values.itertuples(index=False)}
    
    return missing_dict

def create_binary_multi_level_features_list(data, features_info_df):
    bin_list = [] 
    multi_level_list = [] 
    available_features = []
    cat_features = features_info_df[features_info_df['type'] == 'categorical']['attribute'].tolist()
    
    for f in cat_features:
        if f in data:
            available_features.append(f)
    
    cat_variable = data[available_features]
    
    for t in available_features: 
        if len(cat_variable[t].value_counts()) == 2 and  (
            set(cat_variable[t]) & set([0, 1]) == set([0, 1]) or 
            set(cat_variable[t]) & set([0.0, 1.0]) == set([0.0, 1.0])
        ): 
            bin_list.append(t) 
        else: 
            multi_level_list.append(t)
    
    return bin_list, multi_level_list
In [24]:
def clean_data(df):
    """
    Perform feature trimming, re-encoding, and engineering for demographics
    data
    
    INPUT: Demographics DataFrame
    OUTPUT: Trimmed and cleaned demographics DataFrame
    """
    
    available_features = pd.read_csv('AZDIAS_Feature_Summary.csv', delimiter=';')
    missing_features_dict = get_missing_features_dict_from_features_info(available_features)
    
    # Put in code here to execute all main cleaning steps:
    # convert missing value codes into NaNs, ...
    for key,value in missing_features_dict.items():
        df[key].replace(value, inplace=True)
    
    # remove selected columns and rows, ...
    
    #remove columns
    columns_to_be_dropped = ['AGER_TYP','GEBURTSJAHR','TITEL_KZ','KK_KUNDENTYP','KBA05_BAUMAX','ALTER_HH', 
                             'LP_LEBENSPHASE_GROB', 'PLZ8_BAUMAX','LP_LEBENSPHASE_FEIN', 'WOHNLAGE']
    
    df.drop(columns_to_be_dropped, axis=1, inplace=True)
    
    #drop every row has more that 2 NaNs
    df = df[df.isnull().sum(axis=1) < 2].reset_index(drop=True)
    
    # get binary/ multilevel features lists
    binary_features_list, multilevel_features_list =  create_binary_multi_level_features_list(df, available_features)
    
    #encode features for binary_features_list
    df= pd.get_dummies( df, columns = binary_features_list ,drop_first=True)
        
    # drop multi-level
    for col in multi_level_list:
        df.drop(col, axis=1, inplace=True)
        
    
    # Investigate "CAMEO_INTL_2015" and engineer two new variables.
    
    df['CAMEO_INTL_2015_WEALTH'] = df['CAMEO_INTL_2015'].apply(get_wealth)
    df['CAMEO_INTL_2015_LIFE_STAGE'] = df['CAMEO_INTL_2015'].apply(get_life_stage)

    df.drop('CAMEO_INTL_2015', axis=1, inplace=True)

    # Investigate "PRAEGENDE_JUGENDJAHRE" and engineer two new variables.
    first_var_dict = {
        -1:{'period': np.nan, 'direction': np.nan},
        0:{'period': np.nan, 'direction': np.nan},
        1: {'period': 40, 'direction': 'EW'},
        2: {'period': 40, 'direction': 'EW'},
        3: {'period': 50, 'direction': 'EW'},
        4: {'period': 50, 'direction': 'EW'},
        5: {'period': 60, 'direction': 'EW'},
        6: {'period': 60, 'direction': 'W'},
        7: {'period': 60, 'direction': 'E'},
        8: {'period': 70, 'direction': 'EW'},
        9: {'period': 70, 'direction': 'EW'},
        10: {'period': 80, 'direction': 'W'},
        11: {'period': 80, 'direction': 'W'},
        12: {'period': 80, 'direction': 'E'},
        13: {'period': 80, 'direction': 'E'},
        14: {'period': 90, 'direction': 'EW'},
        15: {'period': 90, 'direction': 'EW'}
    }

    #initialize two columns
    df['PRAEGENDE_JUGENDJAHRE_PERIOD']= 0
    df['PRAEGENDE_JUGENDJAHRE_DIRECTION'] = 'x'


    for key, values in first_var_dict.items():
        row_indexes = df[df['PRAEGENDE_JUGENDJAHRE'] == key].index
        df.loc[row_indexes,'PRAEGENDE_JUGENDJAHRE_PERIOD']= values['period']
        df.loc[row_indexes,'PRAEGENDE_JUGENDJAHRE_DIRECTION']= values['direction']

    # check missing
    print(f'missing values for [PRAEGENDE_JUGENDJAHRE_PERIOD]: ', df[df['PRAEGENDE_JUGENDJAHRE_PERIOD'] == 0].shape[0])
    print('missing values for [PRAEGENDE_JUGENDJAHRE_DIRECTION]: ', df[df['PRAEGENDE_JUGENDJAHRE_DIRECTION'] == 'x'].shape[0])

    # drop the original column
    df.drop(['PRAEGENDE_JUGENDJAHRE'], axis=1, inplace=True)
    
    #create dummy from direction
    df = pd.get_dummies( df, columns = ['PRAEGENDE_JUGENDJAHRE_DIRECTION'] ,drop_first=True)
    
    
    # final validation 
    #check if columns still there column to be dropped
    
    mixed_features_df = available_features.query('type == "mixed"')[['attribute','type']]

    columns = set(mixed_features_df['attribute'].tolist())
    columns = columns  - set(columns_to_be_dropped) - set(['PRAEGENDE_JUGENDJAHRE', 'CAMEO_INTL_2015'])

    df.drop(columns, axis=1, inplace=True)
    
    columns in set(df.columns)

    features = available_features[['attribute','type']]
    columns = features['attribute'].tolist()

    dict_of_remaining_cols = {}

    for col in columns:
        if col in df or df.columns.str.startswith(col).any():
            df_columns = df.filter(regex=f'{col}.*').columns.tolist()
            dict_of_remaining_cols[col] = { 
                'category' :features[features['attribute'] == col ].iloc[0][1],
                'columns': df_columns
            }

    for c in columns_to_be_dropped:
        if c in dict_of_remaining_cols.keys() and dict_of_remaining_cols[c]['category']== 'categorical':
            df.drop(dict_of_remaining_cols[c]['columns'], axis=1, inplace=True)
    
    
    #df['GEBAEUDETYP_5.0'].value_counts()
    # drop the below column because 
    #0.0    697108
    #1.0         1
    #Name: GEBAEUDETYP_5.0, dtype: int64
    # and this column doesnt exist after cleanup the clustomer data 
    if 'GEBAEUDETYP_5.0' in df.columns:
        df.drop(['GEBAEUDETYP_5.0'], axis=1, inplace=True) 
    
    # Return the cleaned dataframe.
    return df
    
In [25]:
clean_df = clean_data(pd.read_csv('Udacity_AZDIAS_Subset.csv', delimiter=';'))
clean_df.shape
/opt/conda/lib/python3.6/site-packages/ipykernel_launcher.py:37: SettingWithCopyWarning: 
A value is trying to be set on a copy of a slice from a DataFrame.
Try using .loc[row_indexer,col_indexer] = value instead

See the caveats in the documentation: http://pandas.pydata.org/pandas-docs/stable/indexing.html#indexing-view-versus-copy
missing values for [PRAEGENDE_JUGENDJAHRE_PERIOD]:  6226
missing values for [PRAEGENDE_JUGENDJAHRE_DIRECTION]:  6226
Out[25]:
(638948, 63)
In [26]:
set(clean_df.columns.tolist()) - set(df_with_dummies.columns.tolist())
Out[26]:
set()

Step 2: Feature Transformation

Step 2.1: Apply Feature Scaling

Before we apply dimensionality reduction techniques to the data, we need to perform feature scaling so that the principal component vectors are not influenced by the natural differences in scale for features. Starting from this part of the project, you'll want to keep an eye on the API reference page for sklearn to help you navigate to all of the classes and functions that you'll need. In this substep, you'll need to check the following:

  • sklearn requires that data not have missing values in order for its estimators to work properly. So, before applying the scaler to your data, make sure that you've cleaned the DataFrame of the remaining missing values. This can be as simple as just removing all data points with missing data, or applying an Imputer to replace all missing values. You might also try a more complicated procedure where you temporarily remove missing values in order to compute the scaling parameters before re-introducing those missing values and applying imputation. Think about how much missing data you have and what possible effects each approach might have on your analysis, and justify your decision in the discussion section below.
  • For the actual scaling function, a StandardScaler instance is suggested, scaling each feature to mean 0 and standard deviation 1.
  • For these classes, you can make use of the .fit_transform() method to both fit a procedure to the data as well as apply the transformation to the data at the same time. Don't forget to keep the fit sklearn objects handy, since you'll be applying them to the customer demographics data towards the end of the project.
In [27]:
# If you've not yet cleaned the dataset of all NaN values, then investigate and
# do that now.

clean_df = clean_data(pd.read_csv('Udacity_AZDIAS_Subset.csv', delimiter=';'))
#clean_df.dropna(axis=0, how='any', inplace=True)

fill_NaN = Imputer(missing_values=np.nan, strategy='mean', axis=1)
imputed_df = pd.DataFrame(fill_NaN.fit_transform(clean_df))
imputed_df.columns = clean_df.columns
imputed_df.index = clean_df.index
imputed_df.head()
/opt/conda/lib/python3.6/site-packages/ipykernel_launcher.py:37: SettingWithCopyWarning: 
A value is trying to be set on a copy of a slice from a DataFrame.
Try using .loc[row_indexer,col_indexer] = value instead

See the caveats in the documentation: http://pandas.pydata.org/pandas-docs/stable/indexing.html#indexing-view-versus-copy
missing values for [PRAEGENDE_JUGENDJAHRE_PERIOD]:  6226
missing values for [PRAEGENDE_JUGENDJAHRE_DIRECTION]:  6226
Out[27]:
ALTERSKATEGORIE_GROB FINANZ_MINIMALIST FINANZ_SPARER FINANZ_VORSORGER FINANZ_ANLEGER FINANZ_UNAUFFAELLIGER FINANZ_HAUSBAUER HEALTH_TYP RETOURTYP_BK_S SEMIO_SOZ ... ORTSGR_KLS9 RELAT_AB GREEN_AVANTGARDE_1 SOHO_KZ_1.0 CAMEO_INTL_2015_WEALTH CAMEO_INTL_2015_LIFE_STAGE PRAEGENDE_JUGENDJAHRE_PERIOD PRAEGENDE_JUGENDJAHRE_DIRECTION_EW PRAEGENDE_JUGENDJAHRE_DIRECTION_W PRAEGENDE_JUGENDJAHRE_DIRECTION_x
0 1.0 1.0 5.0 2.0 5.0 4.0 5.0 3.0 1.0 5.0 ... 5.0 4.0 0.0 1.0 5.0 1.0 90.0 1.0 0.0 0.0
1 3.0 1.0 4.0 1.0 2.0 3.0 5.0 3.0 3.0 4.0 ... 5.0 2.0 1.0 0.0 2.0 4.0 90.0 1.0 0.0 0.0
2 3.0 4.0 3.0 4.0 1.0 3.0 2.0 3.0 5.0 6.0 ... 6.0 5.0 0.0 0.0 4.0 3.0 70.0 1.0 0.0 0.0
3 1.0 3.0 1.0 5.0 2.0 2.0 5.0 3.0 3.0 2.0 ... 3.0 3.0 0.0 0.0 5.0 4.0 50.0 1.0 0.0 0.0
4 2.0 1.0 5.0 1.0 5.0 4.0 3.0 2.0 4.0 2.0 ... 6.0 3.0 0.0 0.0 2.0 2.0 80.0 0.0 1.0 0.0

5 rows × 63 columns

In [28]:
# Apply feature scaling to the general population demographics data.

normalizer = StandardScaler()
imputed_df[imputed_df.columns] = normalizer.fit_transform(imputed_df[imputed_df.columns].as_matrix())
imputed_df.head()
Out[28]:
ALTERSKATEGORIE_GROB FINANZ_MINIMALIST FINANZ_SPARER FINANZ_VORSORGER FINANZ_ANLEGER FINANZ_UNAUFFAELLIGER FINANZ_HAUSBAUER HEALTH_TYP RETOURTYP_BK_S SEMIO_SOZ ... ORTSGR_KLS9 RELAT_AB GREEN_AVANTGARDE_1 SOHO_KZ_1.0 CAMEO_INTL_2015_WEALTH CAMEO_INTL_2015_LIFE_STAGE PRAEGENDE_JUGENDJAHRE_PERIOD PRAEGENDE_JUGENDJAHRE_DIRECTION_EW PRAEGENDE_JUGENDJAHRE_DIRECTION_W PRAEGENDE_JUGENDJAHRE_DIRECTION_x
0 -1.746615 -1.509100 1.580890 -1.052777 1.537350 1.049674 1.321729 1.053060 -1.665180 0.391691 ... -0.173454 0.654923 -0.549006 10.821506 1.140899 -1.248658 1.087458 0.567629 -0.484142 -0.099197
1 0.193101 -1.509100 0.902594 -1.772865 -0.526091 0.321756 1.321729 1.053060 -0.292609 -0.120026 ... -0.173454 -0.826003 1.821475 -0.092409 -0.917834 0.749749 1.087458 0.567629 -0.484142 -0.099197
2 0.193101 0.697487 0.224298 0.387398 -1.213905 0.321756 -0.869497 1.053060 1.079962 0.903408 ... 0.262148 1.395386 -0.549006 -0.092409 0.454655 0.083614 -0.150397 0.567629 -0.484142 -0.099197
3 -1.746615 -0.038042 -1.132295 1.107486 -0.526091 -0.406161 1.321729 1.053060 -0.292609 -1.143461 ... -1.044659 -0.085540 -0.549006 -0.092409 1.140899 0.749749 -1.388253 0.567629 -0.484142 -0.099197
4 -0.776757 -1.509100 1.580890 -1.772865 1.537350 1.049674 -0.139088 -0.267127 0.393676 -1.143461 ... 0.262148 -0.085540 -0.549006 -0.092409 -0.917834 -0.582522 0.468531 -1.761713 2.065510 -0.099197

5 rows × 63 columns

Discussion 2.1: Apply Feature Scaling

all features has been scaled but before scalling filled the NaNs with the mean value

Step 2.2: Perform Dimensionality Reduction

On your scaled data, you are now ready to apply dimensionality reduction techniques.

  • Use sklearn's PCA class to apply principal component analysis on the data, thus finding the vectors of maximal variance in the data. To start, you should not set any parameters (so all components are computed) or set a number of components that is at least half the number of features (so there's enough features to see the general trend in variability).
  • Check out the ratio of variance explained by each principal component as well as the cumulative variance explained. Try plotting the cumulative or sequential values using matplotlib's plot() function. Based on what you find, select a value for the number of transformed features you'll retain for the clustering part of the project.
  • Once you've made a choice for the number of components to keep, make sure you re-fit a PCA instance to perform the decided-on transformation.
In [29]:
# pca
def do_pca(n_components, data):
    pca = PCA(n_components)
    X_pca = pca.fit_transform(data)
    return pca, X_pca

#plot relation between components and the ammount of retained information per component
def scree_plot(pca):
    fig, ax = plt.subplots(figsize=(25,7))
    cumsum = np.cumsum(pca.explained_variance_ratio_)

    plt.plot(np.apply_along_axis(lambda x: x*100, 0, cumsum))
    ax.grid()


    plt.xticks(range(0, pca.explained_variance_.shape[0]+10, 10))
    plt.yticks(range(0, 100, 10))
    plt.xlabel('number of components')
    plt.ylabel('cumulative explained variance')
    

# explain variance per component
def explain_variance(pca):
    
    fig, ax = plt.subplots(figsize=(25,7))
    ax.grid()
    
    cumsum = np.cumsum(pca.explained_variance_ratio_)
    
    plt.plot(pca.explained_variance_ratio_, label='variance')
    #plt.plot(cumsum, label='retained information')
    plt.xticks(range(0, pca.explained_variance_.shape[0]+10, 10))
    
    plt.xlabel('number of components')
    plt.ylabel('cumulative explained variance')
    #plt.legend()
    
def pca_results(full_dataset, pca):
    
    dimensions = ['Dimension {}'.format(i) for i in range(1,len(pca.components_)+1)]
    
    # PCA components
    components = pd.DataFrame(np.round(pca.components_, 4), columns = full_dataset.keys())
    components.index = dimensions

    # PCA explained variance
    ratios = pca.explained_variance_ratio_.reshape(len(pca.components_), 1)
    variance_ratios = pd.DataFrame(np.round(ratios, 4), columns = ['Explained Variance'])
    variance_ratios.index = dimensions

    # Return a concatenated DataFrame
    return pd.concat([variance_ratios, components], axis = 1)

def print_weight_vs_feature(pca, dataset_keys):
    
    dimensions = ['Dimension {}'.format(i) for i in range(1,len(pca.components_)+1)]
    components = pd.DataFrame(np.round(pca.components_, 4), columns = dataset_keys)
    # Create a bar plot visualization
    fig, ax = plt.subplots(figsize = (14,8))

    # Plot the feature weights as a function of the components
    components.plot(ax = ax, kind = 'bar');
    ax.set_ylabel("Feature Weights")
    #ax.set_xticklabels(dimensions, rotation=90)


    # Display the explained variance ratios
    for i, ev in enumerate(pca.explained_variance_ratio_):
        ax.text(i-0.40, ax.get_ylim()[1] + 0.05, "Explained Variance\n          %.4f"%(ev))
In [30]:
# Apply PCA to the data.
pca_full, X_pca_full = do_pca(imputed_df.shape[1], imputed_df)
X_pca_full.shape
Out[30]:
(638948, 63)
In [31]:
# Investigate the variance accounted for by each principal component.
scree_plot(pca_full)
explain_variance(pca_full)
In [32]:
# Re-apply PCA to the data while selecting for number of components to retain.
pca_30, X_pca_30 = do_pca(30, imputed_df)
In [33]:
scree_plot(pca_30)
explain_variance(pca_30)
In [34]:
pca_15, X_pca_15 = do_pca(15, imputed_df)
scree_plot(pca_15)
explain_variance(pca_15)
In [35]:
from IPython.core.display import HTML
display(HTML(pca_results(imputed_df, pca_15).T.to_html()))
Dimension 1 Dimension 2 Dimension 3 Dimension 4 Dimension 5 Dimension 6 Dimension 7 Dimension 8 Dimension 9 Dimension 10 Dimension 11 Dimension 12 Dimension 13 Dimension 14 Dimension 15
Explained Variance 0.1722 0.1383 0.0836 0.0485 0.0372 0.0324 0.0303 0.0273 0.0230 0.0213 0.0194 0.0191 0.0184 0.0180 0.0165
ALTERSKATEGORIE_GROB -0.1496 0.2516 0.0715 -0.0319 -0.0008 -0.0235 0.0119 0.0191 0.1452 -0.0289 0.0686 -0.1009 0.0102 -0.0128 -0.0456
FINANZ_MINIMALIST -0.2238 0.0466 0.1696 0.0979 -0.0939 -0.0231 0.0727 0.0484 -0.0453 -0.0211 -0.0726 0.1300 -0.0155 -0.1379 -0.0214
FINANZ_SPARER 0.1671 -0.2223 -0.0994 -0.0245 0.0043 0.0090 -0.0628 0.0561 0.1334 -0.0789 0.0773 -0.1051 0.0415 0.0399 0.0469
FINANZ_VORSORGER -0.1367 0.2188 0.0952 -0.0117 0.0369 -0.0253 0.0551 -0.0331 -0.1446 0.0915 -0.0786 0.0999 -0.0892 0.0333 -0.0354
FINANZ_ANLEGER 0.0855 -0.2034 -0.1912 -0.1518 -0.0254 0.0178 -0.0157 0.0878 0.1319 -0.0029 0.0837 -0.0717 0.0595 -0.0146 0.0206
FINANZ_UNAUFFAELLIGER 0.0985 -0.2201 -0.0888 0.0610 0.0254 -0.0126 -0.1107 0.0447 0.0860 -0.0800 0.1227 -0.1789 -0.0120 0.0334 0.0621
FINANZ_HAUSBAUER 0.1443 0.1303 -0.0601 -0.1511 0.1178 0.0267 -0.1372 -0.1488 -0.1045 0.1641 0.0500 -0.1153 -0.0068 0.1283 -0.0035
HEALTH_TYP 0.0382 -0.0519 -0.0209 0.0178 -0.0258 -0.0185 0.0496 0.1189 0.0581 -0.4243 -0.2351 0.4495 0.0820 0.0921 0.3475
RETOURTYP_BK_S -0.0395 0.1673 0.1158 0.0305 0.0135 -0.0294 0.1119 -0.0320 -0.1554 -0.0648 -0.1552 0.2267 -0.0752 0.0212 -0.0564
SEMIO_SOZ 0.0545 -0.0894 0.2925 -0.0087 -0.0005 0.0097 -0.0153 0.0481 0.0497 -0.0718 -0.0105 -0.0053 0.0373 0.1042 0.0362
SEMIO_FAM 0.0965 -0.1618 0.2829 -0.0194 0.0329 -0.0061 0.0396 -0.0462 -0.0305 -0.0212 -0.0146 0.0274 0.0056 -0.0159 0.0150
SEMIO_REL 0.1335 -0.2497 0.1009 -0.0068 0.0040 0.0074 0.0511 -0.0348 -0.1660 0.0905 -0.0277 0.0320 -0.0439 -0.0341 -0.0198
SEMIO_MAT 0.0995 -0.1447 0.0668 0.0469 0.0655 -0.0155 0.1116 -0.2431 -0.1192 0.1854 -0.0030 -0.0006 -0.0542 -0.1352 -0.2040
SEMIO_VERT -0.0237 -0.0561 0.3803 -0.0145 0.0012 -0.0180 -0.0170 0.0176 0.0696 -0.0255 0.0328 -0.0439 0.0177 0.0283 -0.0079
SEMIO_LUST -0.1069 0.1751 0.0697 0.0052 0.0222 0.0057 -0.0065 -0.0122 0.0965 0.0851 0.1527 -0.2270 -0.0684 -0.1102 -0.1129
SEMIO_ERL -0.1075 0.2187 -0.1960 -0.0090 -0.0227 -0.0107 0.0355 0.0317 0.0988 0.0145 0.0531 -0.0740 0.0023 -0.0233 -0.0533
SEMIO_KULT 0.0942 -0.2038 0.2608 -0.0344 -0.0246 -0.0279 0.0591 0.0437 -0.0945 -0.0052 -0.0268 0.0693 -0.0312 -0.0158 0.0030
SEMIO_RAT 0.1116 -0.1725 -0.2224 0.0652 0.0094 0.0281 0.0692 -0.1136 -0.2027 0.1742 -0.0631 0.0645 -0.0617 -0.0865 -0.0991
SEMIO_KRIT -0.0096 0.1087 -0.3046 0.0338 0.0048 -0.0098 0.0578 -0.0208 -0.0684 -0.1520 -0.0279 0.0713 -0.0208 -0.0452 0.0169
SEMIO_DOM 0.0135 0.0565 -0.3459 0.1363 -0.0165 -0.0020 0.0372 -0.0303 -0.0397 -0.0235 -0.0921 0.1511 0.0247 -0.0031 0.0329
SEMIO_KAEM 0.0268 0.1003 -0.3693 0.0942 -0.0405 -0.0050 0.0432 0.0400 -0.0652 0.0515 -0.0156 0.0095 -0.0291 -0.0011 -0.0388
SEMIO_PFLICHT 0.1362 -0.2294 -0.0659 0.0240 -0.0110 0.0006 0.0955 -0.0107 -0.1840 0.0679 -0.0440 0.0737 -0.0562 -0.0356 0.0258
SEMIO_TRADV 0.1225 -0.2233 -0.0754 0.0796 0.0317 0.0380 0.0728 -0.1536 -0.1957 0.1520 -0.0608 0.0412 -0.0298 -0.0636 -0.0623
ANZ_PERSONEN -0.0756 -0.0655 0.0081 0.0905 -0.1109 0.0329 -0.0498 0.2070 0.2626 0.4956 -0.0794 0.1266 -0.0962 0.0427 0.0497
ANZ_TITEL -0.0050 0.0074 0.0115 0.0517 0.0137 0.0693 -0.0230 -0.0359 0.0268 0.0605 0.5114 0.3754 -0.0890 0.0528 0.0543
HH_EINKOMMEN_SCORE 0.1919 0.0699 -0.0348 -0.2809 0.0462 -0.0778 0.1089 -0.0463 0.0124 -0.0228 0.0375 0.0201 0.0102 0.0495 -0.0022
W_KEIT_KIND_HH 0.0206 0.0388 0.0288 -0.0688 0.0849 -0.0150 -0.0590 -0.1235 -0.2717 -0.3313 0.1752 -0.2449 0.0315 -0.0855 -0.1899
WOHNDAUER_2008 -0.0697 0.0550 0.0395 0.0153 -0.0348 0.0074 0.0780 0.0409 -0.0319 0.2800 -0.1532 0.2141 -0.0505 0.0217 -0.2239
ANZ_HAUSHALTE_AKTIV 0.1277 0.0778 0.0241 -0.0347 0.0747 0.3804 0.0747 0.0849 -0.0273 -0.0178 0.1257 0.0694 0.0463 0.0324 -0.0704
ANZ_HH_TITEL 0.0315 0.0353 0.0144 0.0415 0.0598 0.2561 -0.0071 -0.0242 0.0008 0.0094 0.5372 0.3369 -0.0563 0.0700 -0.0066
KONSUMNAEHE -0.1579 -0.0853 -0.0353 -0.1495 -0.1171 0.0210 0.0465 0.0289 -0.0898 0.0439 -0.0237 0.0023 0.1940 0.2492 -0.0558
MIN_GEBAEUDEJAHR -0.0364 -0.0322 -0.0084 -0.0435 -0.0629 -0.0032 0.0099 -0.0240 0.1058 -0.0043 -0.0975 0.0489 0.2313 -0.3048 0.0398
KBA05_ANTG1 -0.2188 -0.1098 -0.0161 0.1052 -0.1046 0.0159 -0.0496 0.1004 -0.1263 -0.0425 0.0541 -0.0219 0.0020 -0.1822 -0.0111
KBA05_ANTG2 0.0077 -0.0118 -0.0152 0.0004 0.0626 -0.4087 -0.0535 -0.2301 0.0442 -0.0222 0.1720 0.0646 -0.2379 0.0166 0.1532
KBA05_ANTG3 0.1314 0.0608 0.0008 -0.0878 0.0542 -0.2474 -0.0026 -0.1192 0.1383 0.0830 -0.0835 0.0366 -0.0157 0.4119 -0.0296
KBA05_ANTG4 0.1461 0.0908 0.0268 -0.0336 0.0613 0.4327 0.0994 0.1545 -0.0127 0.0021 -0.1043 -0.0678 0.1261 -0.0413 -0.0462
KBA05_GBZ -0.2090 -0.1174 -0.0211 0.0648 -0.1111 -0.1329 -0.0752 -0.0315 -0.0682 -0.0537 0.0889 0.0290 -0.0311 -0.2366 0.0218
BALLRAUM -0.1224 -0.0754 -0.0343 -0.2514 0.0565 -0.0282 0.0850 -0.1346 0.1718 0.0579 0.0455 0.1250 0.2618 -0.1710 -0.0617
EWDICHTE 0.1885 0.1142 0.0431 0.2649 -0.0713 -0.0640 -0.0702 0.1175 -0.1334 -0.0256 0.0019 -0.0476 -0.0703 0.0727 0.0235
INNENSTADT -0.1595 -0.0945 -0.0413 -0.2303 0.0910 0.0053 0.0764 -0.1234 0.1470 0.0523 0.0401 0.0786 0.1662 -0.0830 -0.0457
GEBAEUDETYP_RASTER -0.1131 -0.0573 -0.0305 -0.0951 -0.0837 -0.1837 0.0274 0.0709 -0.1204 0.0900 -0.0133 -0.0065 0.2157 0.3304 -0.0697
KKK 0.0420 0.0101 -0.0206 -0.2946 -0.1790 -0.1511 0.1946 0.4032 -0.1144 0.0435 0.1125 -0.0208 -0.1441 -0.0468 0.0010
MOBI_REGIO -0.2342 -0.1208 -0.0234 0.0730 -0.1132 -0.0734 -0.0661 0.0304 -0.0861 -0.0264 0.0548 0.0091 -0.0164 -0.1670 0.0043
ONLINE_AFFINITAET -0.0210 -0.1658 -0.0461 0.1364 -0.0950 0.0302 -0.1070 0.1630 0.2541 0.1565 0.0400 -0.1086 -0.0233 0.0257 0.1043
REGIOTYP 0.0703 0.0312 -0.0106 -0.2255 -0.1482 -0.1561 0.1894 0.4109 -0.1420 0.0201 0.1425 -0.0652 -0.2508 -0.0937 0.0012
KBA13_ANZAHL_PKW -0.0791 -0.0463 -0.0205 0.0524 0.5264 -0.1011 0.0301 0.2465 -0.0148 0.0455 -0.0141 0.0199 0.0560 -0.0168 -0.0259
PLZ8_ANTG1 -0.2184 -0.1194 -0.0409 -0.0462 0.0140 0.0620 0.0295 0.0480 -0.1504 -0.0417 0.0355 -0.0486 -0.0374 0.1911 0.0205
PLZ8_ANTG2 0.1458 0.0802 0.0258 0.0976 0.0582 -0.2946 -0.0367 -0.0986 0.1183 0.0496 0.0530 0.0635 -0.0525 -0.1106 0.0275
PLZ8_ANTG3 0.2180 0.1209 0.0400 0.0916 0.0268 -0.1217 -0.0363 -0.0015 0.1264 0.0546 -0.0383 0.0330 0.0322 -0.1180 -0.0179
PLZ8_ANTG4 0.2111 0.1167 0.0419 0.0765 0.0060 0.0764 -0.0124 0.0598 0.1096 0.0231 -0.0848 0.0094 0.0330 -0.1386 -0.0385
PLZ8_HHZ 0.0380 0.0167 0.0049 0.1410 0.5054 -0.1252 0.0029 0.2908 -0.0334 0.0468 0.0039 0.0264 0.1376 -0.0978 -0.0300
PLZ8_GBZ -0.1642 -0.0943 -0.0345 0.0391 0.3880 -0.1447 0.0170 0.1688 -0.1116 0.0143 0.0541 0.0086 0.0971 0.0415 0.0043
ARBEIT 0.1387 0.0838 0.0278 0.0473 -0.2015 -0.1655 -0.0570 0.0729 -0.1542 0.0870 0.1343 0.0436 0.4798 -0.0387 -0.0403
ORTSGR_KLS9 0.1911 0.1138 0.0428 0.2488 -0.1080 -0.0456 -0.0751 0.1473 -0.1472 -0.0166 0.0251 -0.0564 -0.0014 0.0698 0.0268
RELAT_AB 0.1289 0.0800 0.0270 0.0854 -0.1746 -0.1812 -0.0334 0.0377 -0.1055 0.0659 0.1579 0.0793 0.4689 -0.0540 -0.0534
GREEN_AVANTGARDE_1 -0.1055 -0.0297 0.0551 0.3625 -0.0449 0.0157 -0.0668 0.0050 0.0498 0.0130 0.0688 -0.0878 0.0762 0.0272 -0.0218
SOHO_KZ_1.0 -0.0019 -0.0024 0.0007 0.0018 -0.0031 0.0014 0.0017 0.0066 0.0089 0.0158 0.0041 -0.0025 0.0066 0.0322 -0.0556
CAMEO_INTL_2015_WEALTH 0.1972 0.0962 0.0188 -0.1507 -0.0002 -0.0832 0.0954 0.0010 0.0947 0.0216 0.0154 0.0557 0.0398 -0.1584 -0.0286
CAMEO_INTL_2015_LIFE_STAGE -0.1266 -0.0174 -0.0082 0.0702 -0.0045 0.0585 -0.0997 -0.0315 -0.1465 0.0830 0.0532 -0.0525 0.1231 0.3242 0.0169
PRAEGENDE_JUGENDJAHRE_PERIOD 0.1154 -0.2391 -0.0994 0.0466 -0.0483 -0.0219 -0.0633 0.1068 0.2134 -0.1802 0.0302 0.0079 0.0022 0.0588 -0.2193
PRAEGENDE_JUGENDJAHRE_DIRECTION_EW 0.0118 0.0262 -0.0079 -0.2218 0.0342 0.0253 -0.5922 0.0909 -0.1239 0.0324 -0.0920 0.1373 -0.0822 -0.0727 -0.0462
PRAEGENDE_JUGENDJAHRE_DIRECTION_W -0.0257 -0.0397 -0.0004 0.2397 -0.0233 -0.0649 0.5589 -0.0509 0.1544 -0.1055 0.0867 -0.0994 0.0070 0.1206 -0.1280
PRAEGENDE_JUGENDJAHRE_DIRECTION_x -0.0005 0.0638 0.0186 -0.0364 0.0644 0.0833 0.1632 -0.1081 -0.1591 0.2333 0.0646 -0.2113 0.1071 -0.0866 0.7468

Discussion 2.2: Perform Dimensionality Reduction

The way i see the accumelated retained information after 50 principle component start to vanish, as at 50 it explains almost 95% of the entire features.

im going to use 20 compenents as it explains a a almost 70% of the variability in the dataset which i beleive is good number and it retains high ratio of information. i choose not to follow the elbow role which is around the 5th pc as it explains 50% of the variability in the dataset which is quite low.

Step 2.3: Interpret Principal Components

Now that we have our transformed principal components, it's a nice idea to check out the weight of each variable on the first few components to see if they can be interpreted in some fashion.

As a reminder, each principal component is a unit vector that points in the direction of highest variance (after accounting for the variance captured by earlier principal components). The further a weight is from zero, the more the principal component is in the direction of the corresponding feature. If two features have large weights of the same sign (both positive or both negative), then increases in one tend expect to be associated with increases in the other. To contrast, features with different signs can be expected to show a negative correlation: increases in one variable should result in a decrease in the other.

  • To investigate the features, you should map each weight to their corresponding feature name, then sort the features according to weight. The most interesting features for each principal component, then, will be those at the beginning and end of the sorted list. Use the data dictionary document to help you understand these most prominent features, their relationships, and what a positive or negative value on the principal component might indicate.
  • You should investigate and interpret feature associations from the first three principal components in this substep. To help facilitate this, you should write a function that you can call at any time to print the sorted list of feature weights, for the i-th principal component. This might come in handy in the next step of the project, when you interpret the tendencies of the discovered clusters.
In [36]:
def pc_weight_per_feature(pca, dataset_df, what=0):
    indecies = [f'{i}' for i in range(1, 16)]
    df = pd.DataFrame(pca.components_, columns=dataset_df.columns, index=indecies)
    df.index.name = 'level'
    if what == 0: 
        return df 
    else:
        s = df.iloc[what-1].sort_values(ascending=False)
        return s

def convert_to_ordered_dict(d):
    from collections import OrderedDict
    return OrderedDict(sorted(d.items(), key=lambda x:x[1], reverse=True))
In [41]:
pc_weights = pc_weight_per_feature(pca_15, imputed_df)
pc_weights.head(3)
Out[41]:
ALTERSKATEGORIE_GROB FINANZ_MINIMALIST FINANZ_SPARER FINANZ_VORSORGER FINANZ_ANLEGER FINANZ_UNAUFFAELLIGER FINANZ_HAUSBAUER HEALTH_TYP RETOURTYP_BK_S SEMIO_SOZ ... ORTSGR_KLS9 RELAT_AB GREEN_AVANTGARDE_1 SOHO_KZ_1.0 CAMEO_INTL_2015_WEALTH CAMEO_INTL_2015_LIFE_STAGE PRAEGENDE_JUGENDJAHRE_PERIOD PRAEGENDE_JUGENDJAHRE_DIRECTION_EW PRAEGENDE_JUGENDJAHRE_DIRECTION_W PRAEGENDE_JUGENDJAHRE_DIRECTION_x
level
1 -0.149597 -0.223834 0.167107 -0.136678 0.085466 0.098488 0.144330 0.038154 -0.039453 0.054470 ... 0.191112 0.128863 -0.105459 -0.001941 0.197216 -0.126604 0.115370 0.011808 -0.025747 -0.000464
2 0.251578 0.046578 -0.222272 0.218785 -0.203371 -0.220076 0.130296 -0.051936 0.167272 -0.089376 ... 0.113803 0.079961 -0.029748 -0.002442 0.096156 -0.017361 -0.239085 0.026190 -0.039677 0.063804
3 0.071464 0.169625 -0.099364 0.095176 -0.191224 -0.088811 -0.060129 -0.020870 0.115834 0.292521 ... 0.042772 0.026983 0.055136 0.000723 0.018822 -0.008183 -0.099369 -0.007938 -0.000375 0.018580

3 rows × 63 columns

In [42]:
# Map weights for the first principal component to corresponding feature names
# and then print the linked values, sorted by weight.
# HINT: Try defining a function here or in a new cell that you can reuse in the
# other cells.

pc1_weights = pc_weight_per_feature(pca_15, imputed_df, 1)
pc1_top_5 = pc1_weights.keys()[1:6].tolist()
pc1_lower_5 = pc1_weights.keys()[-6:-1].tolist()

#convert_to_ordered_dict(pc_weights)
print(pc1_weights)
PLZ8_ANTG3                            0.217963
PLZ8_ANTG4                            0.211148
CAMEO_INTL_2015_WEALTH                0.197216
HH_EINKOMMEN_SCORE                    0.191853
ORTSGR_KLS9                           0.191112
EWDICHTE                              0.188461
FINANZ_SPARER                         0.167107
KBA05_ANTG4                           0.146077
PLZ8_ANTG2                            0.145802
FINANZ_HAUSBAUER                      0.144330
ARBEIT                                0.138744
SEMIO_PFLICHT                         0.136181
SEMIO_REL                             0.133493
KBA05_ANTG3                           0.131432
RELAT_AB                              0.128863
ANZ_HAUSHALTE_AKTIV                   0.127729
SEMIO_TRADV                           0.122523
PRAEGENDE_JUGENDJAHRE_PERIOD          0.115370
SEMIO_RAT                             0.111607
SEMIO_MAT                             0.099501
FINANZ_UNAUFFAELLIGER                 0.098488
SEMIO_FAM                             0.096460
SEMIO_KULT                            0.094231
FINANZ_ANLEGER                        0.085466
REGIOTYP                              0.070277
SEMIO_SOZ                             0.054470
KKK                                   0.042018
HEALTH_TYP                            0.038154
PLZ8_HHZ                              0.037986
ANZ_HH_TITEL                          0.031457
                                        ...   
PRAEGENDE_JUGENDJAHRE_DIRECTION_EW    0.011808
KBA05_ANTG2                           0.007717
PRAEGENDE_JUGENDJAHRE_DIRECTION_x    -0.000464
SOHO_KZ_1.0                          -0.001941
ANZ_TITEL                            -0.004994
SEMIO_KRIT                           -0.009559
ONLINE_AFFINITAET                    -0.020992
SEMIO_VERT                           -0.023674
PRAEGENDE_JUGENDJAHRE_DIRECTION_W    -0.025747
MIN_GEBAEUDEJAHR                     -0.036427
RETOURTYP_BK_S                       -0.039453
WOHNDAUER_2008                       -0.069749
ANZ_PERSONEN                         -0.075626
KBA13_ANZAHL_PKW                     -0.079066
GREEN_AVANTGARDE_1                   -0.105459
SEMIO_LUST                           -0.106945
SEMIO_ERL                            -0.107486
GEBAEUDETYP_RASTER                   -0.113144
BALLRAUM                             -0.122422
CAMEO_INTL_2015_LIFE_STAGE           -0.126604
FINANZ_VORSORGER                     -0.136678
ALTERSKATEGORIE_GROB                 -0.149597
KONSUMNAEHE                          -0.157868
INNENSTADT                           -0.159521
PLZ8_GBZ                             -0.164171
KBA05_GBZ                            -0.209017
PLZ8_ANTG1                           -0.218447
KBA05_ANTG1                          -0.218817
FINANZ_MINIMALIST                    -0.223834
MOBI_REGIO                           -0.234198
Name: 1, Length: 63, dtype: float64
In [43]:
# Map weights for the second principal component to corresponding feature names
# and then print the linked values, sorted by weight.
pc2_weights = pc_weight_per_feature(pca_15, imputed_df, 2)
pc2_top_5 = pc2_weights.keys()[1:6].tolist()
pc2_lower_5 = pc2_weights.keys()[-6:-1].tolist()
#convert_to_ordered_dict(pc_weights)
pc2_weights
Out[43]:
ALTERSKATEGORIE_GROB                  0.251578
FINANZ_VORSORGER                      0.218785
SEMIO_ERL                             0.218674
SEMIO_LUST                            0.175079
RETOURTYP_BK_S                        0.167272
FINANZ_HAUSBAUER                      0.130296
PLZ8_ANTG3                            0.120907
PLZ8_ANTG4                            0.116717
EWDICHTE                              0.114183
ORTSGR_KLS9                           0.113803
SEMIO_KRIT                            0.108729
SEMIO_KAEM                            0.100295
CAMEO_INTL_2015_WEALTH                0.096156
KBA05_ANTG4                           0.090804
ARBEIT                                0.083794
PLZ8_ANTG2                            0.080208
RELAT_AB                              0.079961
ANZ_HAUSHALTE_AKTIV                   0.077773
HH_EINKOMMEN_SCORE                    0.069901
PRAEGENDE_JUGENDJAHRE_DIRECTION_x     0.063804
KBA05_ANTG3                           0.060796
SEMIO_DOM                             0.056479
WOHNDAUER_2008                        0.054999
FINANZ_MINIMALIST                     0.046578
W_KEIT_KIND_HH                        0.038807
ANZ_HH_TITEL                          0.035320
REGIOTYP                              0.031215
PRAEGENDE_JUGENDJAHRE_DIRECTION_EW    0.026190
PLZ8_HHZ                              0.016650
KKK                                   0.010076
                                        ...   
CAMEO_INTL_2015_LIFE_STAGE           -0.017361
GREEN_AVANTGARDE_1                   -0.029748
MIN_GEBAEUDEJAHR                     -0.032154
PRAEGENDE_JUGENDJAHRE_DIRECTION_W    -0.039677
KBA13_ANZAHL_PKW                     -0.046312
HEALTH_TYP                           -0.051936
SEMIO_VERT                           -0.056148
GEBAEUDETYP_RASTER                   -0.057306
ANZ_PERSONEN                         -0.065451
BALLRAUM                             -0.075385
KONSUMNAEHE                          -0.085326
SEMIO_SOZ                            -0.089376
PLZ8_GBZ                             -0.094279
INNENSTADT                           -0.094510
KBA05_ANTG1                          -0.109832
KBA05_GBZ                            -0.117429
PLZ8_ANTG1                           -0.119436
MOBI_REGIO                           -0.120826
SEMIO_MAT                            -0.144656
SEMIO_FAM                            -0.161799
ONLINE_AFFINITAET                    -0.165817
SEMIO_RAT                            -0.172494
FINANZ_ANLEGER                       -0.203371
SEMIO_KULT                           -0.203773
FINANZ_UNAUFFAELLIGER                -0.220076
FINANZ_SPARER                        -0.222272
SEMIO_TRADV                          -0.223304
SEMIO_PFLICHT                        -0.229351
PRAEGENDE_JUGENDJAHRE_PERIOD         -0.239085
SEMIO_REL                            -0.249668
Name: 2, Length: 63, dtype: float64
In [44]:
# Map weights for the third principal component to corresponding feature names
# and then print the linked values, sorted by weight.
pc3_weights = pc_weight_per_feature(pca_15, imputed_df, 3)
pc3_top_5 = pc3_weights.keys()[1:6].tolist()
pc3_lower_5 = pc3_weights.keys()[-6:-1].tolist()
#convert_to_ordered_dict(pc_weights)
pc3_weights
Out[44]:
SEMIO_VERT                           0.380298
SEMIO_SOZ                            0.292521
SEMIO_FAM                            0.282879
SEMIO_KULT                           0.260834
FINANZ_MINIMALIST                    0.169625
RETOURTYP_BK_S                       0.115834
SEMIO_REL                            0.100936
FINANZ_VORSORGER                     0.095176
ALTERSKATEGORIE_GROB                 0.071464
SEMIO_LUST                           0.069736
SEMIO_MAT                            0.066791
GREEN_AVANTGARDE_1                   0.055136
EWDICHTE                             0.043109
ORTSGR_KLS9                          0.042772
PLZ8_ANTG4                           0.041853
PLZ8_ANTG3                           0.039957
WOHNDAUER_2008                       0.039483
W_KEIT_KIND_HH                       0.028842
ARBEIT                               0.027769
RELAT_AB                             0.026983
KBA05_ANTG4                          0.026846
PLZ8_ANTG2                           0.025830
ANZ_HAUSHALTE_AKTIV                  0.024124
CAMEO_INTL_2015_WEALTH               0.018822
PRAEGENDE_JUGENDJAHRE_DIRECTION_x    0.018580
ANZ_HH_TITEL                         0.014441
ANZ_TITEL                            0.011496
ANZ_PERSONEN                         0.008132
PLZ8_HHZ                             0.004946
KBA05_ANTG3                          0.000805
                                       ...   
CAMEO_INTL_2015_LIFE_STAGE          -0.008183
MIN_GEBAEUDEJAHR                    -0.008444
REGIOTYP                            -0.010645
KBA05_ANTG2                         -0.015215
KBA05_ANTG1                         -0.016127
KBA13_ANZAHL_PKW                    -0.020477
KKK                                 -0.020597
HEALTH_TYP                          -0.020870
KBA05_GBZ                           -0.021119
MOBI_REGIO                          -0.023449
GEBAEUDETYP_RASTER                  -0.030477
BALLRAUM                            -0.034310
PLZ8_GBZ                            -0.034454
HH_EINKOMMEN_SCORE                  -0.034786
KONSUMNAEHE                         -0.035271
PLZ8_ANTG1                          -0.040935
INNENSTADT                          -0.041255
ONLINE_AFFINITAET                   -0.046084
FINANZ_HAUSBAUER                    -0.060129
SEMIO_PFLICHT                       -0.065862
SEMIO_TRADV                         -0.075442
FINANZ_UNAUFFAELLIGER               -0.088811
FINANZ_SPARER                       -0.099364
PRAEGENDE_JUGENDJAHRE_PERIOD        -0.099369
FINANZ_ANLEGER                      -0.191224
SEMIO_ERL                           -0.195961
SEMIO_RAT                           -0.222374
SEMIO_KRIT                          -0.304589
SEMIO_DOM                           -0.345904
SEMIO_KAEM                          -0.369273
Name: 3, Length: 63, dtype: float64
In [45]:
pc30_weights = pc_weight_per_feature(pca_15, imputed_df, 15)
pc3_top_5 = pc30_weights.keys()[1:6].tolist()
pc3_lower_5 = pc30_weights.keys()[-6:-1].tolist()
#convert_to_ordered_dict(pc_weights)
print(pc3_weights)


print('-'*22, 'TOP 5', '-'*22)
print(f'pc1: {pc1_top_5}')
print(f'pc2: {pc2_top_5}')
print(f'pc3: {pc3_top_5}')
print('-'*22, 'LOW 5', '-'*22)
print(f'pc1: {pc1_lower_5}')
print(f'pc2: {pc2_lower_5}')
print(f'pc3: {pc3_lower_5}')
SEMIO_VERT                           0.380298
SEMIO_SOZ                            0.292521
SEMIO_FAM                            0.282879
SEMIO_KULT                           0.260834
FINANZ_MINIMALIST                    0.169625
RETOURTYP_BK_S                       0.115834
SEMIO_REL                            0.100936
FINANZ_VORSORGER                     0.095176
ALTERSKATEGORIE_GROB                 0.071464
SEMIO_LUST                           0.069736
SEMIO_MAT                            0.066791
GREEN_AVANTGARDE_1                   0.055136
EWDICHTE                             0.043109
ORTSGR_KLS9                          0.042772
PLZ8_ANTG4                           0.041853
PLZ8_ANTG3                           0.039957
WOHNDAUER_2008                       0.039483
W_KEIT_KIND_HH                       0.028842
ARBEIT                               0.027769
RELAT_AB                             0.026983
KBA05_ANTG4                          0.026846
PLZ8_ANTG2                           0.025830
ANZ_HAUSHALTE_AKTIV                  0.024124
CAMEO_INTL_2015_WEALTH               0.018822
PRAEGENDE_JUGENDJAHRE_DIRECTION_x    0.018580
ANZ_HH_TITEL                         0.014441
ANZ_TITEL                            0.011496
ANZ_PERSONEN                         0.008132
PLZ8_HHZ                             0.004946
KBA05_ANTG3                          0.000805
                                       ...   
CAMEO_INTL_2015_LIFE_STAGE          -0.008183
MIN_GEBAEUDEJAHR                    -0.008444
REGIOTYP                            -0.010645
KBA05_ANTG2                         -0.015215
KBA05_ANTG1                         -0.016127
KBA13_ANZAHL_PKW                    -0.020477
KKK                                 -0.020597
HEALTH_TYP                          -0.020870
KBA05_GBZ                           -0.021119
MOBI_REGIO                          -0.023449
GEBAEUDETYP_RASTER                  -0.030477
BALLRAUM                            -0.034310
PLZ8_GBZ                            -0.034454
HH_EINKOMMEN_SCORE                  -0.034786
KONSUMNAEHE                         -0.035271
PLZ8_ANTG1                          -0.040935
INNENSTADT                          -0.041255
ONLINE_AFFINITAET                   -0.046084
FINANZ_HAUSBAUER                    -0.060129
SEMIO_PFLICHT                       -0.065862
SEMIO_TRADV                         -0.075442
FINANZ_UNAUFFAELLIGER               -0.088811
FINANZ_SPARER                       -0.099364
PRAEGENDE_JUGENDJAHRE_PERIOD        -0.099369
FINANZ_ANLEGER                      -0.191224
SEMIO_ERL                           -0.195961
SEMIO_RAT                           -0.222374
SEMIO_KRIT                          -0.304589
SEMIO_DOM                           -0.345904
SEMIO_KAEM                          -0.369273
Name: 3, Length: 63, dtype: float64
---------------------- TOP 5 ----------------------
pc1: ['PLZ8_ANTG4', 'CAMEO_INTL_2015_WEALTH', 'HH_EINKOMMEN_SCORE', 'ORTSGR_KLS9', 'EWDICHTE']
pc2: ['FINANZ_VORSORGER', 'SEMIO_ERL', 'SEMIO_LUST', 'RETOURTYP_BK_S', 'FINANZ_HAUSBAUER']
pc3: ['HEALTH_TYP', 'KBA05_ANTG2', 'ONLINE_AFFINITAET', 'FINANZ_UNAUFFAELLIGER', 'ANZ_TITEL']
---------------------- LOW 5 ----------------------
pc1: ['PLZ8_GBZ', 'KBA05_GBZ', 'PLZ8_ANTG1', 'KBA05_ANTG1', 'FINANZ_MINIMALIST']
pc2: ['FINANZ_UNAUFFAELLIGER', 'FINANZ_SPARER', 'SEMIO_TRADV', 'SEMIO_PFLICHT', 'PRAEGENDE_JUGENDJAHRE_PERIOD']
pc3: ['SEMIO_LUST', 'PRAEGENDE_JUGENDJAHRE_DIRECTION_W', 'W_KEIT_KIND_HH', 'SEMIO_MAT', 'PRAEGENDE_JUGENDJAHRE_PERIOD']

Discussion 2.3: Interpret Principal Components

based on the privious results for the first 3 pc's we could say

  1. for every pc level their is different features that contributes more than other in creating the biggest variance or explains the most variance. the variance is not affected by the sign as the sign represents direction not magnitude. so the main contributers are the top 5 features and the lower 5 features.

  2. top 5 and lower 5 is highly correlated as they have been loaded in the SAME Principal Component (Eigenvector). which means that an increase in one the remaining ones tend to increase as well.

Step 3: Clustering

Step 3.1: Apply Clustering to General Population

You've assessed and cleaned the demographics data, then scaled and transformed them. Now, it's time to see how the data clusters in the principal components space. In this substep, you will apply k-means clustering to the dataset and use the average within-cluster distances from each point to their assigned cluster's centroid to decide on a number of clusters to keep.

  • Use sklearn's KMeans class to perform k-means clustering on the PCA-transformed data.
  • Then, compute the average difference from each point to its assigned cluster's center. Hint: The KMeans object's .score() method might be useful here, but note that in sklearn, scores tend to be defined so that larger is better. Try applying it to a small, toy dataset, or use an internet search to help your understanding.
  • Perform the above two steps for a number of different cluster counts. You can then see how the average distance decreases with an increasing number of clusters. However, each additional cluster provides a smaller net benefit. Use this fact to select a final number of clusters in which to group the data. Warning: because of the large size of the dataset, it can take a long time for the algorithm to resolve. The more clusters to fit, the longer the algorithm will take. You should test for cluster counts through at least 10 clusters to get the full picture, but you shouldn't need to test for a number of clusters above about 30.
  • Once you've selected a final number of clusters to use, re-fit a KMeans instance to perform the clustering operation. Make sure that you also obtain the cluster assignments for the general demographics data, since you'll be using them in the final Step 3.3.
In [46]:
def get_kmeans_score(data, center):

    kmeans = KMeans(n_clusters=center, random_state=42)
    model = kmeans.fit(data)
    score = np.abs(model.score(data))
    
    return model, score
In [47]:
# Over a number of different cluster counts...
# run k-means clustering on the data and...
# compute the average within-cluster distances.

scores = []
models = []

centers = list(range(1,27, 3))

for center in centers:
    model, score = get_kmeans_score(X_pca_15, center)
    labels = model.predict(X_pca_15)
    scores.append(score)
    models.append(model)
In [48]:
# Investigate the change in within-cluster distance across number of clusters.
# HINT: Use matplotlib's plot function to visualize this relationship.

plt.plot(centers, scores, linestyle='--', marker='o', color='b');
plt.xlabel('K');
plt.ylabel('SSE');
plt.title('SSE vs. K');
In [50]:
# Re-fit the k-means model with the selected number of clusters and obtain
# cluster predictions for the general population demographics data.

kmeans = KMeans(n_clusters=15)
model = kmeans.fit(X_pca_15)
predictions = model.predict(X_pca_15)

Discussion 3.1: Apply Clustering to General Population

from the above plot, its not clear where is the cut-off point as the SSE continues to minimize smoothly, but we can see also that around 15 the SSE decrease almost negligible, which lead me to choose 15 clusters

Step 3.2: Apply All Steps to the Customer Data

Now that you have clusters and cluster centers for the general population, it's time to see how the customer data maps on to those clusters. Take care to not confuse this for re-fitting all of the models to the customer data. Instead, you're going to use the fits from the general population to clean, transform, and cluster the customer data. In the last step of the project, you will interpret how the general population fits apply to the customer data.

  • Don't forget when loading in the customers data, that it is semicolon (;) delimited.
  • Apply the same feature wrangling, selection, and engineering steps to the customer demographics using the clean_data() function you created earlier. (You can assume that the customer demographics data has similar meaning behind missing data patterns as the general demographics data.)
  • Use the sklearn objects from the general demographics data, and apply their transformations to the customers data. That is, you should not be using a .fit() or .fit_transform() method to re-fit the old objects, nor should you be creating new sklearn objects! Carry the data through the feature scaling, PCA, and clustering steps, obtaining cluster assignments for all of the data in the customer demographics data.
In [51]:
# Load in the customer demographics data.
customers = pd.read_csv('Udacity_CUSTOMERS_Subset.csv', delimiter=';')
print(customers.shape)
customers.head(3)
(191652, 85)
Out[51]:
AGER_TYP ALTERSKATEGORIE_GROB ANREDE_KZ CJT_GESAMTTYP FINANZ_MINIMALIST FINANZ_SPARER FINANZ_VORSORGER FINANZ_ANLEGER FINANZ_UNAUFFAELLIGER FINANZ_HAUSBAUER ... PLZ8_ANTG1 PLZ8_ANTG2 PLZ8_ANTG3 PLZ8_ANTG4 PLZ8_BAUMAX PLZ8_HHZ PLZ8_GBZ ARBEIT ORTSGR_KLS9 RELAT_AB
0 2 4 1 5.0 5 1 5 1 2 2 ... 3.0 3.0 1.0 0.0 1.0 5.0 5.0 1.0 2.0 1.0
1 -1 4 1 NaN 5 1 5 1 3 2 ... NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN
2 -1 4 2 2.0 5 1 5 1 4 4 ... 2.0 3.0 3.0 1.0 3.0 3.0 2.0 3.0 5.0 3.0

3 rows × 85 columns

In [52]:
# Apply preprocessing, feature transformation, and clustering from the general
# demographics onto the customer data, obtaining cluster predictions for the
# customer demographics data.
clean_customers_df =clean_data(customers)
print(clean_customers_df.shape)
clean_customers_df.head(3)
/opt/conda/lib/python3.6/site-packages/ipykernel_launcher.py:37: SettingWithCopyWarning: 
A value is trying to be set on a copy of a slice from a DataFrame.
Try using .loc[row_indexer,col_indexer] = value instead

See the caveats in the documentation: http://pandas.pydata.org/pandas-docs/stable/indexing.html#indexing-view-versus-copy
missing values for [PRAEGENDE_JUGENDJAHRE_PERIOD]:  314
missing values for [PRAEGENDE_JUGENDJAHRE_DIRECTION]:  314
(116609, 63)
Out[52]:
ALTERSKATEGORIE_GROB FINANZ_MINIMALIST FINANZ_SPARER FINANZ_VORSORGER FINANZ_ANLEGER FINANZ_UNAUFFAELLIGER FINANZ_HAUSBAUER HEALTH_TYP RETOURTYP_BK_S SEMIO_SOZ ... ORTSGR_KLS9 RELAT_AB GREEN_AVANTGARDE_1 SOHO_KZ_1.0 CAMEO_INTL_2015_WEALTH CAMEO_INTL_2015_LIFE_STAGE PRAEGENDE_JUGENDJAHRE_PERIOD PRAEGENDE_JUGENDJAHRE_DIRECTION_EW PRAEGENDE_JUGENDJAHRE_DIRECTION_W PRAEGENDE_JUGENDJAHRE_DIRECTION_x
0 4.0 5 1 5 1 2 2 1.0 5.0 6 ... 2.0 1.0 1 0 1 3 50.0 1 0 0
1 4.0 5 1 5 1 4 4 2.0 5.0 2 ... 5.0 3.0 1 0 3 4 50.0 1 0 0
2 3.0 3 1 4 4 5 2 3.0 5.0 4 ... 5.0 1.0 0 0 4 1 70.0 1 0 0

3 rows × 63 columns

In [54]:
clean_customers_df[clean_customers_df.columns].shape

#remove NaNs
fill_NaN = Imputer(missing_values=np.nan, strategy='mean', axis=1)
df = pd.DataFrame(fill_NaN.fit_transform(clean_customers_df))
df.columns = clean_customers_df.columns
df.index = clean_customers_df.index
df.head()


df[df.columns] = normalizer.transform(df[df.columns].as_matrix())

#transform the customers data using pca object
X_customer_pca = pca_15.transform(df)
#predict clustering using the kmeans object
predict_customers = model.predict(X_customer_pca)

print(df.shape)
df.head()
(116609, 63)
Out[54]:
ALTERSKATEGORIE_GROB FINANZ_MINIMALIST FINANZ_SPARER FINANZ_VORSORGER FINANZ_ANLEGER FINANZ_UNAUFFAELLIGER FINANZ_HAUSBAUER HEALTH_TYP RETOURTYP_BK_S SEMIO_SOZ ... ORTSGR_KLS9 RELAT_AB GREEN_AVANTGARDE_1 SOHO_KZ_1.0 CAMEO_INTL_2015_WEALTH CAMEO_INTL_2015_LIFE_STAGE PRAEGENDE_JUGENDJAHRE_PERIOD PRAEGENDE_JUGENDJAHRE_DIRECTION_EW PRAEGENDE_JUGENDJAHRE_DIRECTION_W PRAEGENDE_JUGENDJAHRE_DIRECTION_x
0 1.162959 1.433016 -1.132295 1.107486 -1.213905 -0.406161 -0.869497 -1.587315 1.079962 0.903408 ... -1.480262 -1.566467 1.821475 -0.092409 -1.604078 0.083614 -1.388253 0.567629 -0.484142 -0.099197
1 1.162959 1.433016 -1.132295 1.107486 -1.213905 1.049674 0.591320 -0.267127 1.079962 -1.143461 ... -0.173454 -0.085540 1.821475 -0.092409 -0.231589 0.749749 -1.388253 0.567629 -0.484142 -0.099197
2 0.193101 -0.038042 -1.132295 0.387398 0.849536 1.777591 -0.869497 1.053060 1.079962 -0.120026 ... -0.173454 -1.566467 -0.549006 -0.092409 0.454655 -1.248658 -0.150397 0.567629 -0.484142 -0.099197
3 0.193101 1.433016 -1.132295 1.107486 -1.213905 -0.406161 -0.139088 1.053060 -0.292609 0.903408 ... 0.697751 1.395386 1.821475 -0.092409 -0.231589 0.749749 -1.388253 0.567629 -0.484142 -0.099197
4 1.162959 1.433016 -1.132295 1.107486 -1.213905 -1.134078 -0.869497 -0.267127 1.079962 -0.120026 ... -1.044659 -0.826003 1.821475 -0.092409 -0.917834 0.083614 -1.388253 0.567629 -0.484142 -0.099197

5 rows × 63 columns

Step 3.3: Compare Customer Data to Demographics Data

At this point, you have clustered data based on demographics of the general population of Germany, and seen how the customer data for a mail-order sales company maps onto those demographic clusters. In this final substep, you will compare the two cluster distributions to see where the strongest customer base for the company is.

Consider the proportion of persons in each cluster for the general population, and the proportions for the customers. If we think the company's customer base to be universal, then the cluster assignment proportions should be fairly similar between the two. If there are only particular segments of the population that are interested in the company's products, then we should see a mismatch from one to the other. If there is a higher proportion of persons in a cluster for the customer data compared to the general population (e.g. 5% of persons are assigned to a cluster for the general population, but 15% of the customer data is closest to that cluster's centroid) then that suggests the people in that cluster to be a target audience for the company. On the other hand, the proportion of the data in a cluster being larger in the general population than the customer data (e.g. only 2% of customers closest to a population centroid that captures 6% of the data) suggests that group of persons to be outside of the target demographics.

Take a look at the following points in this step:

  • Compute the proportion of data points in each cluster for the general population and the customer data. Visualizations will be useful here: both for the individual dataset proportions, but also to visualize the ratios in cluster representation between groups. Seaborn's countplot() or barplot() function could be handy.
    • Recall the analysis you performed in step 1.1.3 of the project, where you separated out certain data points from the dataset if they had more than a specified threshold of missing values. If you found that this group was qualitatively different from the main bulk of the data, you should treat this as an additional data cluster in this analysis. Make sure that you account for the number of data points in this subset, for both the general population and customer datasets, when making your computations!
  • Which cluster or clusters are overrepresented in the customer dataset compared to the general population? Select at least one such cluster and infer what kind of people might be represented by that cluster. Use the principal component interpretations from step 2.3 or look at additional components to help you make this inference. Alternatively, you can use the .inverse_transform() method of the PCA and StandardScaler objects to transform centroids back to the original data space and interpret the retrieved values directly.
  • Perform a similar investigation for the underrepresented clusters. Which cluster or clusters are underrepresented in the customer dataset compared to the general population, and what kinds of people are typified by these clusters?
In [56]:
# Compare the proportion of data in each cluster for the customer data to the
# proportion of data in each cluster for the general population.

general_prop = []
customers_prop = []
x = [i+1 for i in range(15)]
for i in range(15):
    general_prop.append((predictions == i).sum()/len(predictions))
    customers_prop.append((predict_customers == i).sum()/len(predict_customers))


    
fig, ax = plt.subplots()
df_general = pd.DataFrame({'cluster' : x, 'general' : general_prop, 'customers':customers_prop})
df_general.plot(x='cluster', y = ['general', 'customers'], kind='bar', figsize=(20,10), ax=ax)
plt.ylabel('proportion of persons in each cluster')
ax.grid()
plt.show()
In [59]:
# What kinds of people are part of a cluster that is overrepresented in the
# customer data compared to the general population?

# the 10th cluster is overrepresented

data1_10 = normalizer.inverse_transform(pca_15.inverse_transform(X_customer_pca[np.where(predict_customers==9)])).round()

df = pd.DataFrame(data=data1_10,
          index=np.array(range(0, data1_10.shape[0])),
          columns=df.columns)
df.head(20)
Out[59]:
ALTERSKATEGORIE_GROB FINANZ_MINIMALIST FINANZ_SPARER FINANZ_VORSORGER FINANZ_ANLEGER FINANZ_UNAUFFAELLIGER FINANZ_HAUSBAUER HEALTH_TYP RETOURTYP_BK_S SEMIO_SOZ ... ORTSGR_KLS9 RELAT_AB GREEN_AVANTGARDE_1 SOHO_KZ_1.0 CAMEO_INTL_2015_WEALTH CAMEO_INTL_2015_LIFE_STAGE PRAEGENDE_JUGENDJAHRE_PERIOD PRAEGENDE_JUGENDJAHRE_DIRECTION_EW PRAEGENDE_JUGENDJAHRE_DIRECTION_W PRAEGENDE_JUGENDJAHRE_DIRECTION_x
0 4.0 5.0 1.0 5.0 0.0 2.0 3.0 1.0 4.0 6.0 ... 3.0 1.0 1.0 0.0 1.0 4.0 55.0 1.0 0.0 -0.0
1 3.0 5.0 1.0 4.0 1.0 2.0 2.0 3.0 4.0 5.0 ... 6.0 4.0 1.0 0.0 3.0 4.0 61.0 1.0 0.0 0.0
2 4.0 5.0 1.0 5.0 0.0 2.0 3.0 1.0 4.0 6.0 ... 5.0 3.0 1.0 0.0 2.0 4.0 55.0 1.0 0.0 -0.0
3 3.0 5.0 2.0 4.0 2.0 3.0 1.0 3.0 3.0 6.0 ... 6.0 4.0 1.0 0.0 1.0 4.0 72.0 1.0 0.0 0.0
4 3.0 5.0 2.0 4.0 1.0 3.0 2.0 1.0 3.0 6.0 ... 4.0 1.0 1.0 0.0 1.0 4.0 61.0 1.0 0.0 0.0
5 3.0 5.0 1.0 5.0 1.0 2.0 2.0 1.0 4.0 5.0 ... 5.0 2.0 1.0 0.0 1.0 5.0 62.0 0.0 1.0 0.0
6 3.0 5.0 1.0 5.0 1.0 2.0 2.0 1.0 4.0 6.0 ... 4.0 2.0 1.0 0.0 1.0 5.0 56.0 1.0 0.0 0.0
7 3.0 4.0 3.0 3.0 3.0 4.0 1.0 2.0 2.0 6.0 ... 5.0 1.0 1.0 0.0 1.0 3.0 87.0 1.0 0.0 -0.0
8 3.0 4.0 2.0 4.0 1.0 2.0 3.0 1.0 4.0 5.0 ... 7.0 4.0 1.0 0.0 3.0 3.0 66.0 1.0 0.0 -0.0
9 4.0 5.0 1.0 4.0 1.0 2.0 1.0 1.0 4.0 5.0 ... 5.0 3.0 1.0 0.0 2.0 3.0 64.0 -0.0 1.0 -0.0
10 3.0 5.0 2.0 4.0 2.0 2.0 1.0 3.0 4.0 6.0 ... 5.0 3.0 1.0 0.0 2.0 4.0 66.0 1.0 -0.0 -0.0
11 3.0 5.0 1.0 5.0 1.0 2.0 1.0 3.0 5.0 5.0 ... 4.0 1.0 1.0 0.0 1.0 4.0 65.0 0.0 1.0 0.0
12 4.0 4.0 3.0 3.0 2.0 4.0 2.0 1.0 2.0 6.0 ... 7.0 4.0 1.0 0.0 2.0 4.0 80.0 -0.0 1.0 0.0
13 4.0 6.0 0.0 5.0 -0.0 1.0 2.0 1.0 5.0 5.0 ... 5.0 2.0 1.0 0.0 2.0 4.0 58.0 0.0 1.0 -0.0
14 3.0 4.0 2.0 4.0 2.0 3.0 2.0 3.0 4.0 5.0 ... 4.0 2.0 0.0 0.0 3.0 2.0 73.0 1.0 0.0 -0.0
15 4.0 5.0 1.0 5.0 1.0 1.0 2.0 2.0 5.0 5.0 ... 6.0 4.0 1.0 0.0 3.0 3.0 59.0 -0.0 1.0 0.0
16 3.0 5.0 2.0 4.0 1.0 2.0 2.0 2.0 4.0 5.0 ... 6.0 2.0 1.0 0.0 2.0 3.0 66.0 1.0 0.0 -0.0
17 3.0 5.0 0.0 5.0 0.0 2.0 2.0 1.0 4.0 5.0 ... 7.0 4.0 1.0 0.0 1.0 5.0 52.0 1.0 -0.0 0.0
18 3.0 5.0 1.0 5.0 0.0 1.0 3.0 3.0 5.0 5.0 ... 7.0 5.0 1.0 0.0 3.0 4.0 57.0 1.0 0.0 0.0
19 4.0 5.0 1.0 5.0 1.0 2.0 1.0 2.0 4.0 6.0 ... 4.0 1.0 1.0 0.0 1.0 4.0 63.0 -0.0 1.0 0.0

20 rows × 63 columns

In [60]:
#general part

data_2_10 = normalizer.inverse_transform(pca_15.inverse_transform(X_pca_15[np.where(predictions==9)])).round()

df = pd.DataFrame(data=data_2_10,
          index=np.array(range(0, data_2_10.shape[0])),
          columns=df.columns)
df.head(20)
Out[60]:
ALTERSKATEGORIE_GROB FINANZ_MINIMALIST FINANZ_SPARER FINANZ_VORSORGER FINANZ_ANLEGER FINANZ_UNAUFFAELLIGER FINANZ_HAUSBAUER HEALTH_TYP RETOURTYP_BK_S SEMIO_SOZ ... ORTSGR_KLS9 RELAT_AB GREEN_AVANTGARDE_1 SOHO_KZ_1.0 CAMEO_INTL_2015_WEALTH CAMEO_INTL_2015_LIFE_STAGE PRAEGENDE_JUGENDJAHRE_PERIOD PRAEGENDE_JUGENDJAHRE_DIRECTION_EW PRAEGENDE_JUGENDJAHRE_DIRECTION_W PRAEGENDE_JUGENDJAHRE_DIRECTION_x
0 3.0 4.0 3.0 3.0 2.0 3.0 2.0 3.0 3.0 5.0 ... 5.0 3.0 1.0 0.0 2.0 3.0 83.0 -0.0 1.0 -0.0
1 3.0 4.0 2.0 4.0 1.0 2.0 2.0 3.0 4.0 5.0 ... 6.0 4.0 1.0 -0.0 3.0 3.0 68.0 1.0 0.0 -0.0
2 3.0 4.0 3.0 2.0 2.0 3.0 1.0 3.0 3.0 5.0 ... 8.0 5.0 1.0 -0.0 4.0 2.0 82.0 -0.0 1.0 0.0
3 3.0 4.0 2.0 4.0 2.0 3.0 2.0 1.0 3.0 4.0 ... 4.0 2.0 1.0 0.0 2.0 4.0 74.0 -0.0 1.0 -0.0
4 4.0 5.0 2.0 4.0 2.0 3.0 1.0 2.0 3.0 5.0 ... 6.0 3.0 1.0 0.0 3.0 2.0 73.0 0.0 1.0 0.0
5 3.0 5.0 2.0 4.0 2.0 2.0 2.0 3.0 4.0 6.0 ... 7.0 3.0 1.0 0.0 2.0 4.0 70.0 1.0 0.0 -0.0
6 3.0 6.0 2.0 3.0 2.0 2.0 -0.0 3.0 4.0 5.0 ... 2.0 2.0 1.0 0.0 2.0 2.0 77.0 -0.0 1.0 0.0
7 3.0 4.0 2.0 4.0 2.0 2.0 2.0 3.0 4.0 5.0 ... 4.0 2.0 1.0 0.0 2.0 3.0 74.0 0.0 1.0 -0.0
8 3.0 4.0 2.0 4.0 1.0 2.0 3.0 3.0 4.0 6.0 ... 7.0 4.0 1.0 0.0 2.0 4.0 65.0 1.0 0.0 -0.0
9 4.0 5.0 1.0 5.0 1.0 2.0 3.0 1.0 4.0 5.0 ... 8.0 3.0 1.0 0.0 2.0 5.0 53.0 1.0 0.0 0.0
10 4.0 5.0 1.0 5.0 1.0 2.0 3.0 1.0 4.0 6.0 ... 5.0 2.0 1.0 0.0 1.0 5.0 57.0 1.0 0.0 0.0
11 3.0 5.0 2.0 4.0 2.0 3.0 1.0 3.0 4.0 5.0 ... 5.0 4.0 1.0 0.0 2.0 4.0 74.0 -0.0 1.0 -0.0
12 3.0 5.0 2.0 4.0 2.0 3.0 1.0 3.0 4.0 6.0 ... 5.0 2.0 1.0 0.0 2.0 4.0 75.0 0.0 1.0 -0.0
13 3.0 4.0 3.0 3.0 3.0 3.0 1.0 3.0 3.0 6.0 ... 8.0 5.0 1.0 -0.0 3.0 3.0 76.0 1.0 -0.0 0.0
14 3.0 5.0 2.0 4.0 2.0 3.0 1.0 3.0 4.0 5.0 ... 5.0 2.0 1.0 0.0 2.0 3.0 78.0 -0.0 1.0 -0.0
15 2.0 4.0 3.0 3.0 3.0 3.0 1.0 2.0 3.0 5.0 ... 3.0 2.0 1.0 0.0 2.0 3.0 82.0 -0.0 1.0 -0.0
16 3.0 5.0 2.0 4.0 1.0 2.0 2.0 2.0 4.0 6.0 ... 7.0 3.0 1.0 0.0 3.0 3.0 64.0 1.0 0.0 -0.0
17 2.0 4.0 4.0 2.0 3.0 4.0 1.0 2.0 2.0 7.0 ... 7.0 4.0 1.0 0.0 2.0 4.0 91.0 0.0 1.0 0.0
18 4.0 5.0 1.0 5.0 1.0 2.0 3.0 1.0 4.0 6.0 ... 5.0 3.0 1.0 0.0 2.0 4.0 56.0 1.0 0.0 -0.0
19 3.0 5.0 3.0 3.0 3.0 3.0 1.0 3.0 3.0 6.0 ... 5.0 4.0 1.0 0.0 2.0 4.0 82.0 -0.0 1.0 -0.0

20 rows × 63 columns

In [61]:
# What kinds of people are part of a cluster that is underrepresented in the
# customer data compared to the general population?

# cluster 8

data_1_8 = normalizer.inverse_transform(pca_15.inverse_transform(X_customer_pca[np.where(predict_customers==7)])).round()

df = pd.DataFrame(data=data_1_8,
          index=np.array(range(0, data_1_8.shape[0])),
          columns=df.columns)
df.head(20)
Out[61]:
ALTERSKATEGORIE_GROB FINANZ_MINIMALIST FINANZ_SPARER FINANZ_VORSORGER FINANZ_ANLEGER FINANZ_UNAUFFAELLIGER FINANZ_HAUSBAUER HEALTH_TYP RETOURTYP_BK_S SEMIO_SOZ ... ORTSGR_KLS9 RELAT_AB GREEN_AVANTGARDE_1 SOHO_KZ_1.0 CAMEO_INTL_2015_WEALTH CAMEO_INTL_2015_LIFE_STAGE PRAEGENDE_JUGENDJAHRE_PERIOD PRAEGENDE_JUGENDJAHRE_DIRECTION_EW PRAEGENDE_JUGENDJAHRE_DIRECTION_W PRAEGENDE_JUGENDJAHRE_DIRECTION_x
0 2.0 3.0 2.0 5.0 2.0 1.0 4.0 3.0 6.0 3.0 ... 8.0 4.0 -0.0 0.0 4.0 2.0 60.0 1.0 -0.0 0.0
1 1.0 1.0 5.0 2.0 4.0 4.0 4.0 3.0 2.0 3.0 ... 7.0 4.0 0.0 0.0 3.0 4.0 90.0 1.0 -0.0 0.0
2 2.0 1.0 4.0 2.0 4.0 4.0 4.0 3.0 3.0 3.0 ... 6.0 3.0 0.0 0.0 4.0 3.0 89.0 1.0 -0.0 -0.0
3 2.0 1.0 4.0 2.0 4.0 4.0 4.0 1.0 2.0 2.0 ... 8.0 5.0 0.0 0.0 4.0 2.0 86.0 1.0 -0.0 -0.0
4 1.0 1.0 4.0 2.0 3.0 4.0 4.0 3.0 3.0 3.0 ... 9.0 4.0 0.0 0.0 3.0 3.0 87.0 1.0 -0.0 -0.0
5 1.0 1.0 5.0 2.0 4.0 4.0 5.0 2.0 2.0 3.0 ... 8.0 3.0 -0.0 0.0 5.0 2.0 89.0 1.0 0.0 0.0
6 2.0 2.0 3.0 3.0 3.0 2.0 4.0 2.0 3.0 3.0 ... 8.0 4.0 0.0 0.0 5.0 2.0 76.0 1.0 -0.0 -0.0
7 2.0 1.0 3.0 4.0 2.0 2.0 5.0 2.0 4.0 3.0 ... 8.0 4.0 0.0 0.0 4.0 3.0 70.0 1.0 0.0 0.0
8 1.0 1.0 5.0 1.0 4.0 4.0 4.0 3.0 2.0 4.0 ... 9.0 4.0 -0.0 -0.0 5.0 1.0 93.0 1.0 -0.0 0.0
9 1.0 1.0 5.0 1.0 5.0 4.0 4.0 2.0 2.0 3.0 ... 8.0 4.0 0.0 0.0 5.0 1.0 91.0 1.0 0.0 0.0
10 2.0 2.0 3.0 3.0 3.0 3.0 4.0 2.0 4.0 3.0 ... 8.0 3.0 0.0 0.0 4.0 2.0 77.0 0.0 0.0 0.0
11 1.0 1.0 5.0 2.0 4.0 4.0 4.0 3.0 3.0 4.0 ... 9.0 4.0 0.0 0.0 5.0 2.0 92.0 1.0 0.0 -0.0
12 1.0 0.0 5.0 1.0 5.0 5.0 4.0 2.0 2.0 4.0 ... 8.0 3.0 0.0 0.0 5.0 2.0 97.0 1.0 0.0 -0.0
13 2.0 2.0 4.0 2.0 3.0 4.0 4.0 2.0 2.0 3.0 ... 7.0 4.0 1.0 0.0 3.0 3.0 85.0 1.0 -0.0 -0.0
14 1.0 2.0 4.0 2.0 4.0 4.0 3.0 3.0 3.0 3.0 ... 6.0 2.0 0.0 0.0 3.0 2.0 89.0 1.0 -0.0 -0.0
15 2.0 2.0 3.0 4.0 2.0 2.0 4.0 2.0 4.0 3.0 ... 8.0 4.0 0.0 0.0 5.0 2.0 70.0 1.0 0.0 -0.0
16 2.0 3.0 2.0 4.0 2.0 2.0 4.0 2.0 5.0 3.0 ... 9.0 4.0 0.0 0.0 5.0 1.0 70.0 0.0 0.0 0.0
17 1.0 0.0 6.0 1.0 5.0 5.0 4.0 2.0 1.0 3.0 ... 6.0 4.0 -0.0 0.0 5.0 1.0 96.0 1.0 0.0 0.0
18 2.0 2.0 3.0 3.0 3.0 3.0 3.0 2.0 4.0 4.0 ... 9.0 4.0 0.0 0.0 5.0 1.0 79.0 -0.0 1.0 0.0
19 1.0 1.0 5.0 2.0 5.0 4.0 4.0 3.0 2.0 3.0 ... 5.0 3.0 -0.0 0.0 4.0 2.0 87.0 1.0 -0.0 0.0

20 rows × 63 columns

In [62]:
#general 

data_2_8 = normalizer.inverse_transform(pca_15.inverse_transform(X_pca_15[np.where(predictions==7)])).round()

df = pd.DataFrame(data=data_2_8,
          index=np.array(range(0, data_2_8.shape[0])),
          columns=df.columns)
df.head(20)
Out[62]:
ALTERSKATEGORIE_GROB FINANZ_MINIMALIST FINANZ_SPARER FINANZ_VORSORGER FINANZ_ANLEGER FINANZ_UNAUFFAELLIGER FINANZ_HAUSBAUER HEALTH_TYP RETOURTYP_BK_S SEMIO_SOZ ... ORTSGR_KLS9 RELAT_AB GREEN_AVANTGARDE_1 SOHO_KZ_1.0 CAMEO_INTL_2015_WEALTH CAMEO_INTL_2015_LIFE_STAGE PRAEGENDE_JUGENDJAHRE_PERIOD PRAEGENDE_JUGENDJAHRE_DIRECTION_EW PRAEGENDE_JUGENDJAHRE_DIRECTION_W PRAEGENDE_JUGENDJAHRE_DIRECTION_x
0 1.0 1.0 5.0 2.0 5.0 4.0 4.0 3.0 2.0 4.0 ... 4.0 3.0 -0.0 0.0 4.0 2.0 94.0 1.0 -0.0 -0.0
1 2.0 1.0 4.0 2.0 4.0 4.0 4.0 1.0 3.0 3.0 ... 9.0 4.0 0.0 0.0 5.0 2.0 89.0 -0.0 1.0 -0.0
2 2.0 3.0 2.0 4.0 2.0 2.0 4.0 2.0 4.0 2.0 ... 8.0 4.0 0.0 0.0 4.0 3.0 68.0 1.0 -0.0 0.0
3 2.0 1.0 5.0 2.0 4.0 4.0 4.0 3.0 3.0 4.0 ... 9.0 4.0 0.0 0.0 4.0 2.0 91.0 1.0 -0.0 0.0
4 1.0 1.0 5.0 2.0 4.0 4.0 4.0 3.0 3.0 4.0 ... 8.0 4.0 -0.0 0.0 5.0 2.0 89.0 1.0 -0.0 0.0
5 1.0 0.0 5.0 1.0 5.0 5.0 4.0 3.0 2.0 4.0 ... 9.0 4.0 0.0 0.0 4.0 2.0 93.0 1.0 -0.0 0.0
6 2.0 1.0 4.0 2.0 4.0 4.0 4.0 2.0 3.0 3.0 ... 8.0 4.0 0.0 0.0 5.0 2.0 89.0 -0.0 1.0 0.0
7 1.0 1.0 5.0 2.0 4.0 4.0 4.0 2.0 2.0 3.0 ... 8.0 4.0 0.0 0.0 3.0 4.0 90.0 1.0 -0.0 0.0
8 1.0 2.0 5.0 2.0 5.0 4.0 3.0 2.0 2.0 3.0 ... 6.0 3.0 0.0 0.0 4.0 2.0 91.0 -0.0 1.0 0.0
9 2.0 1.0 5.0 2.0 4.0 4.0 4.0 2.0 3.0 3.0 ... 7.0 5.0 0.0 0.0 5.0 2.0 89.0 -0.0 1.0 0.0
10 2.0 2.0 3.0 4.0 3.0 2.0 4.0 2.0 4.0 3.0 ... 7.0 4.0 -0.0 0.0 5.0 2.0 71.0 1.0 0.0 0.0
11 1.0 1.0 5.0 2.0 4.0 4.0 4.0 2.0 2.0 3.0 ... 7.0 4.0 -0.0 0.0 5.0 1.0 90.0 1.0 0.0 0.0
12 1.0 1.0 4.0 2.0 4.0 4.0 4.0 3.0 3.0 3.0 ... 6.0 5.0 -0.0 0.0 5.0 2.0 88.0 1.0 -0.0 -0.0
13 1.0 1.0 5.0 2.0 5.0 3.0 4.0 3.0 3.0 3.0 ... 6.0 5.0 -0.0 0.0 5.0 2.0 91.0 1.0 -0.0 -0.0
14 2.0 2.0 3.0 3.0 3.0 2.0 4.0 2.0 4.0 3.0 ... 9.0 3.0 0.0 0.0 5.0 1.0 75.0 0.0 0.0 0.0
15 2.0 2.0 4.0 2.0 4.0 4.0 3.0 2.0 2.0 3.0 ... 8.0 4.0 1.0 0.0 3.0 3.0 88.0 -0.0 1.0 0.0
16 1.0 1.0 5.0 2.0 5.0 5.0 5.0 2.0 2.0 3.0 ... 8.0 4.0 0.0 0.0 4.0 3.0 90.0 1.0 0.0 0.0
17 1.0 1.0 5.0 1.0 5.0 5.0 4.0 3.0 2.0 4.0 ... 6.0 4.0 -0.0 0.0 4.0 2.0 93.0 1.0 -0.0 0.0
18 1.0 1.0 5.0 2.0 5.0 4.0 5.0 2.0 2.0 3.0 ... 7.0 5.0 -0.0 0.0 5.0 2.0 89.0 1.0 0.0 -0.0
19 1.0 1.0 5.0 2.0 5.0 4.0 4.0 3.0 2.0 4.0 ... 4.0 4.0 -0.0 0.0 4.0 3.0 93.0 1.0 -0.0 -0.0

20 rows × 63 columns

In [63]:
cust_df = pd.DataFrame(X_customer_pca, columns=np.arange(1, 16))
cust_df['cluster'] = predict_customers
cust_df.describe()
Out[63]:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 cluster
count 116609.000000 116609.000000 116609.000000 116609.000000 116609.000000 116609.000000 116609.000000 116609.000000 116609.000000 116609.000000 116609.000000 116609.000000 116609.000000 116609.000000 116609.000000 116609.000000
mean -2.562325 1.045009 1.350877 0.778240 -0.015058 0.086964 -0.322565 0.040910 0.032596 0.489332 0.066128 0.110077 -0.239823 -0.048712 -0.104548 6.134132
std 2.593779 2.185210 2.046077 1.864759 1.518706 1.321063 1.235363 1.351884 1.165895 1.051161 1.786718 1.430394 1.048925 0.977259 0.734685 4.187678
min -10.460801 -7.508819 -4.508346 -5.975267 -6.272879 -3.415393 -4.054628 -5.602639 -6.374754 -6.329422 -3.066150 -5.563806 -8.791308 -7.766257 -4.036512 0.000000
25% -4.532047 -0.350657 -0.724747 -0.631400 -1.093821 -0.701477 -1.184217 -0.912235 -0.732486 -0.198683 -0.620663 -0.642537 -0.954211 -0.671084 -0.544686 3.000000
50% -3.216142 0.916252 1.988773 0.774432 -0.152822 0.002271 -0.551586 0.090821 0.003004 0.436164 -0.143934 -0.093939 -0.248549 -0.039415 -0.127813 6.000000
75% -0.992660 2.406255 2.988525 2.174784 0.952971 0.717549 0.218909 1.007417 0.784739 1.138537 0.325180 0.587685 0.464607 0.569537 0.277474 9.000000
max 10.399194 11.400199 5.914888 8.610200 7.609500 30.350037 4.515902 6.080259 5.342037 11.786643 57.979978 40.802583 7.810122 8.522459 9.342589 14.000000
In [64]:
g = sns.pairplot(cust_df,
                 vars = np.arange(1, 16), 
                 diag_kind = 'kde',
                 hue="cluster",
                 palette="Paired");

Discussion 3.3: Compare Customer Data to Demographics Data

(Double-click this cell and replace this text with your own text, reporting findings and conclusions from the clustering analysis. Can we describe segments of the population that are relatively popular with the mail-order company, or relatively unpopular with the company?)

cluster 4, 10 are overrepresented in the customer data compared to the general population, while clusters 8, 15 is the clusters that is being underpresented based on customer data.

i think we could safely assume the the people in cluster 4, 9 are the most interesting people from customer base prespective, as for the features role extraction form PCA_15 that Personality typology, affectes positively, on the other hand it could be that the traget customers are likely to be the ones that belongs to those clusters from the general population

also what we could see from the pairplot that the seperation of clustes it clear up to the 4th component, and more uniformally distributed.

Congratulations on making it this far in the project! Before you finish, make sure to check through the entire notebook from top to bottom to make sure that your analysis follows a logical flow and all of your findings are documented in Discussion cells. Once you've checked over all of your work, you should export the notebook as an HTML document to submit for evaluation. You can do this from the menu, navigating to File -> Download as -> HTML (.html). You will submit both that document and this notebook for your project submission.